2021-06-06 15:38:46 +00:00
< ? php
2024-01-25 13:01:25 +00:00
declare ( strict_types = 1 );
namespace Bouncer ;
2021-06-06 15:38:46 +00:00
use AdamBrett\ShellWrapper\Command\Builder as CommandBuilder ;
use AdamBrett\ShellWrapper\Runners\Exec ;
use Aws\S3\S3Client ;
2024-05-18 14:15:55 +00:00
use Bouncer\Logger\AbstractLogger ;
2021-06-06 15:38:46 +00:00
use GuzzleHttp\Client as Guzzle ;
2023-01-09 14:57:00 +00:00
use GuzzleHttp\Exception\ConnectException ;
use GuzzleHttp\Exception\ServerException ;
2021-06-06 15:38:46 +00:00
use League\Flysystem\AwsS3V3\AwsS3V3Adapter ;
use League\Flysystem\FileAttributes ;
use League\Flysystem\Filesystem ;
2024-01-08 00:35:15 +00:00
use League\Flysystem\FilesystemException ;
2021-06-06 15:38:46 +00:00
use League\Flysystem\Local\LocalFilesystemAdapter ;
2024-05-18 14:15:55 +00:00
use Bouncer\Logger\Logger ;
2024-01-25 13:01:25 +00:00
use Bouncer\Logger\Formatter ;
2021-06-06 15:38:46 +00:00
use Spatie\Emoji\Emoji ;
2024-01-05 17:15:51 +00:00
use Symfony\Component\Yaml\Yaml ;
2024-01-25 13:01:25 +00:00
use Twig\Environment as Twig ;
2024-02-08 15:48:07 +00:00
use Twig\Error\LoaderError ;
use Twig\Error\RuntimeError ;
use Twig\Error\SyntaxError ;
2024-01-25 13:01:25 +00:00
use Twig\Loader\FilesystemLoader as TwigLoader ;
use GuzzleHttp\Exception\GuzzleException ;
use Monolog\Processor ;
use Bouncer\Settings\Settings ;
2021-06-06 15:38:46 +00:00
class Bouncer
{
private array $environment ;
2024-01-25 13:01:25 +00:00
private Guzzle $docker ;
private TwigLoader $loader ;
private Twig $twig ;
2021-06-06 15:38:46 +00:00
private Filesystem $configFilesystem ;
private Filesystem $certificateStoreLocal ;
2022-05-05 10:34:04 +00:00
private ? Filesystem $certificateStoreRemote = null ;
2022-08-09 00:28:22 +00:00
private Filesystem $providedCertificateStore ;
2024-05-18 14:15:55 +00:00
private AbstractLogger $logger ;
2024-01-05 14:47:37 +00:00
private array $previousContainerState = [];
2024-01-05 17:16:21 +00:00
private array $previousSwarmState = [];
2021-06-06 15:38:46 +00:00
private array $fileHashes ;
2024-01-05 17:16:21 +00:00
private bool $swarmMode = false ;
private bool $useGlobalCert = false ;
private int $forcedUpdateIntervalSeconds = 0 ;
private ? int $lastUpdateEpoch = null ;
2023-01-09 14:57:00 +00:00
private int $maximumNginxConfigCreationNotices = 15 ;
2024-01-25 13:01:25 +00:00
private Settings $settings ;
2024-01-25 14:37:54 +00:00
private const DEFAULT_DOCKER_SOCKET = '/var/run/docker.sock' ;
private const FILESYSTEM_CONFIG_DIR = '/etc/nginx/sites-enabled' ;
private const FILESYSTEM_CERTS_DIR = '/etc/nginx/certs' ;
private const FILESYSTEM_CERTS_PROVIDED_DIR = '/certs' ;
2022-05-05 14:42:53 +00:00
2021-06-06 15:38:46 +00:00
public function __construct ()
{
$this -> environment = array_merge ( $_ENV , $_SERVER );
ksort ( $this -> environment );
2024-01-25 13:01:25 +00:00
$this -> settings = new Settings ();
2024-05-18 14:15:55 +00:00
$this -> logger = new Logger (
2024-01-25 13:01:25 +00:00
settings : $this -> settings ,
processIdProcessor : new Processor\ProcessIdProcessor (),
memoryPeakUsageProcessor : new Processor\MemoryPeakUsageProcessor (),
psrLogMessageProcessor : new Processor\PsrLogMessageProcessor (),
coloredLineFormatter : new Formatter\ColourLine ( $this -> settings ),
lineFormatter : new Formatter\Line ( $this -> settings ),
);
2021-06-06 15:38:46 +00:00
2022-08-09 00:28:22 +00:00
if ( isset ( $this -> environment [ 'DOCKER_HOST' ])) {
2024-01-25 15:59:48 +00:00
$this -> logger -> info ( 'Connecting to {docker_host}' , [ 'emoji' => Emoji :: electricPlug (), 'docker_host' => $this -> environment [ 'DOCKER_HOST' ]]);
2024-01-25 13:01:25 +00:00
$this -> docker = new Guzzle ([ 'base_uri' => $this -> environment [ 'DOCKER_HOST' ]]);
2022-08-09 00:28:22 +00:00
} else {
2024-01-25 15:59:48 +00:00
$this -> logger -> info ( 'Connecting to {docker_host}' , [ 'emoji' => Emoji :: electricPlug (), 'docker_host' => Bouncer :: DEFAULT_DOCKER_SOCKET ]);
2024-01-25 13:01:25 +00:00
$this -> docker = new Guzzle ([ 'base_uri' => 'http://localhost' , 'curl' => [ CURLOPT_UNIX_SOCKET_PATH => Bouncer :: DEFAULT_DOCKER_SOCKET ]]);
2022-08-09 00:28:22 +00:00
}
2021-06-06 15:38:46 +00:00
2024-01-25 13:01:25 +00:00
$this -> loader = new TwigLoader ([ __DIR__ . '/../templates' ]);
$this -> twig = new Twig ( $this -> loader );
2021-06-06 15:38:46 +00:00
// Set up Filesystem for sites-enabled path
2024-01-25 14:37:54 +00:00
$this -> configFilesystem = new Filesystem ( new LocalFilesystemAdapter ( Bouncer :: FILESYSTEM_CONFIG_DIR ));
2021-06-06 15:38:46 +00:00
// Set up Local certificate store
2024-01-25 14:37:54 +00:00
$this -> certificateStoreLocal = new Filesystem ( new LocalFilesystemAdapter ( Bouncer :: FILESYSTEM_CERTS_DIR ));
2021-06-06 15:38:46 +00:00
2022-08-09 00:28:22 +00:00
// Set up Local certificate store for certificates provided to us
2024-01-25 14:37:54 +00:00
$this -> providedCertificateStore = new Filesystem ( new LocalFilesystemAdapter ( Bouncer :: FILESYSTEM_CERTS_PROVIDED_DIR ));
2022-08-09 00:28:22 +00:00
2021-06-06 15:38:46 +00:00
// Set up Remote certificate store, if configured
2022-05-05 10:34:04 +00:00
if ( isset ( $this -> environment [ 'BOUNCER_S3_BUCKET' ])) {
2021-06-06 15:38:46 +00:00
$this -> certificateStoreRemote = new Filesystem (
new AwsS3V3Adapter (
new S3Client ([
2024-01-05 17:16:21 +00:00
'endpoint' => $this -> environment [ 'BOUNCER_S3_ENDPOINT' ],
2021-06-06 15:38:46 +00:00
'use_path_style_endpoint' => isset ( $this -> environment [ 'BOUNCER_S3_USE_PATH_STYLE_ENDPOINT' ]),
2024-01-05 17:16:21 +00:00
'credentials' => [
'key' => $this -> environment [ 'BOUNCER_S3_KEY_ID' ],
2021-06-06 15:38:46 +00:00
'secret' => $this -> environment [ 'BOUNCER_S3_KEY_SECRET' ],
],
2024-01-05 17:16:21 +00:00
'region' => $this -> environment [ 'BOUNCER_S3_REGION' ] ? ? 'us-east' ,
2021-06-06 15:38:46 +00:00
'version' => 'latest' ,
]),
$this -> environment [ 'BOUNCER_S3_BUCKET' ],
$this -> environment [ 'BOUNCER_S3_PREFIX' ] ? ? ''
)
);
}
}
2023-01-09 14:57:00 +00:00
public function getMaximumNginxConfigCreationNotices () : int
{
return $this -> maximumNginxConfigCreationNotices ;
}
public function setMaximumNginxConfigCreationNotices ( int $maximumNginxConfigCreationNotices ) : Bouncer
{
$this -> maximumNginxConfigCreationNotices = $maximumNginxConfigCreationNotices ;
return $this ;
}
2022-06-15 09:22:24 +00:00
public function isSwarmMode () : bool
{
return $this -> swarmMode ;
}
public function setSwarmMode ( bool $swarmMode ) : Bouncer
{
$this -> swarmMode = $swarmMode ;
return $this ;
}
2022-08-09 00:28:22 +00:00
public function isUseGlobalCert () : bool
{
return $this -> useGlobalCert ;
}
public function setUseGlobalCert ( bool $useGlobalCert ) : Bouncer
{
$this -> useGlobalCert = $useGlobalCert ;
return $this ;
}
public function getForcedUpdateIntervalSeconds () : int
{
return $this -> forcedUpdateIntervalSeconds ;
}
public function setForcedUpdateIntervalSeconds ( int $forcedUpdateIntervalSeconds ) : Bouncer
{
$this -> forcedUpdateIntervalSeconds = $forcedUpdateIntervalSeconds ;
return $this ;
}
2021-06-06 15:38:46 +00:00
/**
2024-01-25 13:01:25 +00:00
* @ return Target []
2024-01-05 14:47:37 +00:00
*
2024-01-25 13:01:25 +00:00
* @ throws GuzzleException
2021-06-06 15:38:46 +00:00
*/
2022-05-05 14:42:53 +00:00
public function findContainersContainerMode () : array
2021-06-06 15:38:46 +00:00
{
$bouncerTargets = [];
2022-05-05 14:42:53 +00:00
2024-01-25 13:01:25 +00:00
$containers = json_decode ( $this -> docker -> request ( 'GET' , 'containers/json' ) -> getBody () -> getContents (), true );
2021-06-06 15:38:46 +00:00
foreach ( $containers as $container ) {
2024-02-08 15:48:07 +00:00
$envs = [];
$container = json_decode ( $this -> docker -> request ( 'GET' , " containers/ { $container [ 'Id' ] } /json " ) -> getBody () -> getContents (), true );
if (
! isset ( $container [ 'Config' ][ 'Env' ])
) {
continue ;
}
// Parse all the environment variables and store them in an array.
foreach ( $container [ 'Config' ][ 'Env' ] as $env ) {
[ $envKey , $envVal ] = explode ( '=' , $env , 2 );
if ( str_starts_with ( $envKey , 'BOUNCER_' )) {
$envs [ $envKey ] = $envVal ;
2021-06-06 15:38:46 +00:00
}
}
2024-02-08 15:48:07 +00:00
ksort ( $envs );
// If there are no BOUNCER_* environment variables, skip this service.
if ( count ( $envs ) == 0 ) {
continue ;
}
// If BOUNCER_IGNORE is set, skip this service.
if ( isset ( $envs [ 'BOUNCER_IGNORE' ])) {
continue ;
}
2021-06-06 15:38:46 +00:00
if ( isset ( $envs [ 'BOUNCER_DOMAIN' ])) {
2024-02-08 15:48:07 +00:00
$bouncerTarget = ( new Target (
logger : $this -> logger ,
settings : $this -> settings ,
))
-> setId ( $container [ 'Id' ])
2021-06-06 15:38:46 +00:00
;
2022-05-05 14:42:53 +00:00
$bouncerTarget = $this -> parseContainerEnvironmentVariables ( $envs , $bouncerTarget );
2021-06-06 15:38:46 +00:00
2024-02-08 15:48:07 +00:00
if ( ! empty ( $container [ 'NetworkSettings' ][ 'IPAddress' ])) {
2021-06-06 15:38:46 +00:00
// As per docker service
2024-02-08 15:48:07 +00:00
$bouncerTarget -> setEndpointHostnameOrIp ( $container [ 'NetworkSettings' ][ 'IPAddress' ]);
2021-06-06 15:38:46 +00:00
} else {
// As per docker compose
2024-02-08 15:48:07 +00:00
$networks = array_values ( $container [ 'NetworkSettings' ][ 'Networks' ]);
2022-08-09 00:28:22 +00:00
$bouncerTarget -> setEndpointHostnameOrIp ( $networks [ 0 ][ 'IPAddress' ]);
2021-06-06 15:38:46 +00:00
}
2024-02-26 16:33:56 +00:00
$bouncerTarget -> setTargetPath ( sprintf ( 'http://%s:%d' , $bouncerTarget -> getEndpointHostnameOrIp (), $bouncerTarget -> getPort () >= 0 ? $bouncerTarget -> getPort () : 80 ));
2021-06-06 15:38:46 +00:00
2022-08-09 00:28:22 +00:00
$bouncerTarget -> setUseGlobalCert ( $this -> isUseGlobalCert ());
$valid = $bouncerTarget -> isEndpointValid ();
// $this->logger->debug(sprintf(
// '%s Decided that %s has the endpoint %s and it %s.',
// Emoji::magnifyingGlassTiltedLeft(),
// $bouncerTarget->getName(),
// $bouncerTarget->getEndpointHostnameOrIp(),
// $valid ? 'is valid' : 'is not valid'
// ));
if ( $valid ) {
2023-01-09 14:57:00 +00:00
$bouncerTargets [] = $bouncerTarget ;
2022-08-09 00:28:22 +00:00
}
2021-06-06 15:38:46 +00:00
}
}
2022-06-15 09:22:24 +00:00
2022-05-05 14:42:53 +00:00
return $bouncerTargets ;
}
2022-06-15 09:22:24 +00:00
2022-05-05 14:42:53 +00:00
public function findContainersSwarmMode () : array
{
$bouncerTargets = [];
2024-01-25 13:01:25 +00:00
$services = json_decode ( $this -> docker -> request ( 'GET' , 'services' ) -> getBody () -> getContents (), true );
2022-05-05 14:42:53 +00:00
2022-06-15 09:22:24 +00:00
if ( isset ( $services [ 'message' ])) {
2024-05-16 11:22:34 +00:00
$this -> logger -> debug ( 'Something happened while interrogating services.. This node is not a swarm node, cannot have services: {message}' , [ 'emoji' => Emoji :: warning () . ' Bouncer.php' , 'message' => $services [ 'message' ]]);
2022-06-15 09:22:24 +00:00
} else {
foreach ( $services as $service ) {
2022-05-05 14:42:53 +00:00
$envs = [];
2022-06-15 09:22:24 +00:00
if (
2022-05-05 15:35:09 +00:00
! isset ( $service [ 'Spec' ])
|| ! isset ( $service [ 'Spec' ][ 'TaskTemplate' ])
|| ! isset ( $service [ 'Spec' ][ 'TaskTemplate' ][ 'ContainerSpec' ])
|| ! isset ( $service [ 'Spec' ][ 'TaskTemplate' ][ 'ContainerSpec' ][ 'Env' ])
2022-06-15 09:22:24 +00:00
) {
2022-05-05 15:35:09 +00:00
continue ;
}
2024-02-08 15:48:07 +00:00
// Parse all the environment variables and store them in an array.
2022-06-15 09:22:24 +00:00
foreach ( $service [ 'Spec' ][ 'TaskTemplate' ][ 'ContainerSpec' ][ 'Env' ] as $env ) {
2024-02-08 15:48:07 +00:00
[ $envKey , $envVal ] = explode ( '=' , $env , 2 );
if ( str_starts_with ( $envKey , 'BOUNCER_' )) {
$envs [ $envKey ] = $envVal ;
}
}
ksort ( $envs );
// If there are no BOUNCER_* environment variables, skip this service.
if ( count ( $envs ) == 0 ) {
continue ;
}
// if BOUNCER_IGNORE is set, skip this service.
if ( isset ( $envs [ 'BOUNCER_IGNORE' ])) {
continue ;
}
$bouncerTarget = ( new Target (
logger : $this -> logger ,
settings : $this -> settings ,
));
if ( isset ( $envs [ 'BOUNCER_LABEL' ])) {
$bouncerTarget -> setLabel ( $envs [ 'BOUNCER_LABEL' ]);
2022-05-05 14:42:53 +00:00
}
2022-06-15 09:22:24 +00:00
if ( isset ( $envs [ 'BOUNCER_DOMAIN' ])) {
2024-02-08 15:48:07 +00:00
$bouncerTarget -> setId ( $service [ 'ID' ]);
$bouncerTarget -> setLabel ( $service [ 'Spec' ][ 'Name' ]);
2022-05-05 14:42:53 +00:00
$bouncerTarget = $this -> parseContainerEnvironmentVariables ( $envs , $bouncerTarget );
2024-02-26 14:50:50 +00:00
if ( $bouncerTarget -> hasCustomNginxConfig ()) {
$this -> logger -> info ( 'Custom nginx config for {label} is provided.' , [ 'emoji' => Emoji :: artistPalette (), 'label' => $bouncerTarget -> getLabel ()]);
$bouncerTargets [] = $bouncerTarget ;
continue ;
}
2022-08-09 00:28:22 +00:00
if ( $bouncerTarget -> isPortSet ()) {
$bouncerTarget -> setEndpointHostnameOrIp ( $service [ 'Spec' ][ 'Name' ]);
2024-02-08 15:48:07 +00:00
// $this->logger->info('{label}: Ports for {target_name} has been explicitly set to {host}:{port}.', ['emoji' => Emoji::warning().' ', 'target_name' => $bouncerTarget->getName(), 'host' => $bouncerTarget->getEndpointHostnameOrIp(), 'port' => $bouncerTarget->getPort()]);
2022-08-09 00:28:22 +00:00
} elseif ( isset ( $service [ 'Endpoint' ][ 'Ports' ])) {
$bouncerTarget -> setEndpointHostnameOrIp ( '172.17.0.1' );
2024-01-25 13:01:25 +00:00
$bouncerTarget -> setPort ( intval ( $service [ 'Endpoint' ][ 'Ports' ][ 0 ][ 'PublishedPort' ]));
2022-08-09 00:28:22 +00:00
} else {
2024-05-16 11:22:34 +00:00
$this -> logger -> warning ( '{label}: ports block missing for {target_name}. Try setting BOUNCER_TARGET_PORT.' , [ 'emoji' => Emoji :: warning () . ' Bouncer.php' , 'label' => $bouncerTarget -> getLabel (), 'target_name' => $bouncerTarget -> getName ()]);
2024-02-08 15:48:07 +00:00
\Kint :: dump (
$bouncerTarget -> getId (),
$bouncerTarget -> getLabel (),
$envs
);
2022-08-09 00:28:22 +00:00
continue ;
}
2024-02-26 16:33:56 +00:00
$bouncerTarget -> setTargetPath ( sprintf ( 'http://%s:%d' , $bouncerTarget -> getEndpointHostnameOrIp (), $bouncerTarget -> getPort ()));
2022-08-09 00:28:22 +00:00
$bouncerTarget -> setUseGlobalCert ( $this -> isUseGlobalCert ());
2022-05-05 14:42:53 +00:00
2024-05-18 14:15:55 +00:00
// @phpstan-ignore-next-line MB: I'm not sure you're right about ->hasCustomNginxConfig only returning false, Stan..
2024-02-26 14:50:50 +00:00
if ( $bouncerTarget -> isEndpointValid () || $bouncerTarget -> hasCustomNginxConfig ()) {
2023-01-09 14:57:00 +00:00
$bouncerTargets [] = $bouncerTarget ;
2024-01-05 14:47:37 +00:00
} else {
2024-01-25 13:01:25 +00:00
$this -> logger -> debug (
2024-01-25 15:59:48 +00:00
'Decided that {target_name} has the endpoint {endpoint} and it is not valid.' ,
2024-01-25 13:01:25 +00:00
[
'emoji' => Emoji :: magnifyingGlassTiltedLeft (),
'target_name' => $bouncerTarget -> getName (),
'endpoint' => $bouncerTarget -> getEndpointHostnameOrIp (),
]
);
2022-08-09 00:28:22 +00:00
}
2022-05-05 14:42:53 +00:00
}
}
}
2021-06-06 15:38:46 +00:00
return $bouncerTargets ;
}
public function run () : void
{
2024-05-16 11:22:34 +00:00
$this -> logger -> info ( 'Starting Bouncer. Built {build_id} on {build_date}, {build_ago}' , [ 'emoji' => Emoji :: redHeart () . ' Bouncer.php' , 'build_id' => $this -> settings -> get ( 'build/id' ), 'build_date' => $this -> settings -> get ( 'build/date' ) -> toDateTimeString (), 'build_ago' => $this -> settings -> get ( 'build/date' ) -> ago ()]);
2024-02-08 15:48:07 +00:00
$this -> logger -> info ( 'Build #{git_sha}: "{build_message}"' , [ 'emoji' => Emoji :: memo (), 'git_sha' => $this -> settings -> get ( 'build/sha_short' ), 'build_message' => $this -> settings -> get ( 'build/message' )]);
$this -> logger -> debug ( ' > HTTPS Listener is on {https_port}' , [ 'emoji' => Emoji :: ship (), 'https_port' => $this -> settings -> get ( 'bouncer/https_port' )]);
$this -> logger -> debug ( ' > HTTP Listener is on {http_port}' , [ 'emoji' => Emoji :: ship (), 'http_port' => $this -> settings -> get ( 'bouncer/http_port' )]);
// Allow defined global cert if set
if ( $this -> settings -> has ( 'ssl/global_cert' ) && $this -> settings -> has ( 'ssl/global_cert_key' )) {
$this -> setUseGlobalCert ( true );
$this -> providedCertificateStore -> write ( 'global.crt' , str_replace ( '\\n' , " \n " , trim ( $this -> settings -> get ( 'ssl/global_cert' ), '"' )));
$this -> providedCertificateStore -> write ( 'global.key' , str_replace ( '\\n' , " \n " , trim ( $this -> settings -> get ( 'ssl/global_cert_key' ), '"' )));
}
$this -> logger -> debug ( ' > Global Cert is {enabled}' , [ 'emoji' => Emoji :: globeShowingEuropeAfrica (), 'enabled' => $this -> isUseGlobalCert () ? 'enabled' : 'disabled' ]);
// Determine forced update interval.
if ( $this -> settings -> has ( 'bouncer/forced_update_interval_seconds' )) {
$this -> setForcedUpdateIntervalSeconds ( $this -> settings -> get ( 'bouncer/forced_update_interval_seconds' ));
}
$this -> logger -> debug ( ' > Forced Update Interval is {state}' , [ 'emoji' => Emoji :: watch (), 'state' => $this -> getForcedUpdateIntervalSeconds () > 0 ? $this -> getForcedUpdateIntervalSeconds () : 'disabled' ]);
// Determine maximum notices for nginx config creation.
if ( $this -> settings -> has ( 'bouncer/max_nginx_config_creation_notices' )) {
$maxConfigCreationNotices = $this -> settings -> get ( 'bouncer/max_nginx_config_creation_notices' );
$originalMaximumNginxConfigCreationNotices = $this -> getMaximumNginxConfigCreationNotices ();
$this -> setMaximumNginxConfigCreationNotices ( $maxConfigCreationNotices );
$this -> logger -> debug ( ' > Maximum Nginx config creation notices has been over-ridden: {original} => {new}' , [ 'emoji' => Emoji :: hikingBoot (), 'original' => $originalMaximumNginxConfigCreationNotices , 'new' => $this -> getMaximumNginxConfigCreationNotices ()]);
}
// State if non-SSL is allowed. This is processed in the Target class.
$this -> logger -> debug ( ' > Allow non-SSL is {enabled}' , [ 'emoji' => Emoji :: ship (), 'enabled' => $this -> settings -> get ( 'ssl/allow_non_ssl' ) ? 'enabled' : 'disabled' ]);
2022-06-15 09:22:24 +00:00
2021-06-06 22:08:49 +00:00
try {
$this -> stateHasChanged ();
2023-01-09 14:57:00 +00:00
} catch ( ConnectException $connectException ) {
2024-02-08 15:48:07 +00:00
$this -> logger -> critical ( 'Could not connect to docker socket! Did you forget to map it?' , [ 'emoji' => Emoji :: cryingCat ()]);
2022-06-15 09:22:24 +00:00
2024-02-08 15:48:07 +00:00
exit ( 1 );
2021-06-06 22:08:49 +00:00
}
2024-05-18 14:15:55 +00:00
// @phpstan-ignore-next-line Yes, I know this is a loop, that is desired.
2021-06-06 15:38:46 +00:00
while ( true ) {
$this -> runLoop ();
}
}
2024-01-25 13:01:25 +00:00
public function parseContainerEnvironmentVariables ( array $envs , Target $bouncerTarget ) : Target
2022-06-15 09:22:24 +00:00
{
2024-02-08 15:48:07 +00:00
// Process label and name specifically before all else.
foreach ( array_filter ( $envs ) as $envKey => $envVal ) {
switch ( $envKey ) {
2024-01-25 15:00:57 +00:00
case 'BOUNCER_LABEL' :
2024-02-08 15:48:07 +00:00
$bouncerTarget -> setLabel ( $envVal );
2024-01-25 13:57:59 +00:00
break ;
2022-06-15 09:22:24 +00:00
case 'BOUNCER_DOMAIN' :
2024-02-08 15:48:07 +00:00
$domains = explode ( ',' , $envVal );
2022-06-15 09:22:24 +00:00
array_walk ( $domains , function ( & $domain , $key ) : void {
$domain = trim ( $domain );
});
$bouncerTarget -> setDomains ( $domains );
break ;
2024-02-08 15:48:07 +00:00
}
}
foreach ( array_filter ( $envs ) as $envKey => $envVal ) {
switch ( $envKey ) {
2023-01-09 14:57:00 +00:00
case 'BOUNCER_AUTH' :
2024-02-08 15:48:07 +00:00
[ $username , $password ] = explode ( ':' , $envVal );
2023-01-09 14:57:00 +00:00
$bouncerTarget -> setAuth ( $username , $password );
2024-02-08 15:48:07 +00:00
// $this->logger->info('{label}: Basic Auth has been enabled.', ['emoji' => Emoji::key(), 'label' => $bouncerTarget->getLabel(),]);
2023-01-09 14:57:00 +00:00
break ;
2024-01-25 10:41:42 +00:00
case 'BOUNCER_HOST_OVERRIDE' :
2024-02-08 15:48:07 +00:00
$bouncerTarget -> setHostOverride ( $envVal );
2024-04-30 14:28:27 +00:00
$this -> logger -> warning ( '{label}: Host reported to container overridden and set to {host_override}.' , [ 'emoji' => Emoji :: hikingBoot (), 'label' => $bouncerTarget -> getLabel (), 'host_override' => $bouncerTarget -> getHostOverride ()]);
2024-01-25 10:41:42 +00:00
break ;
2022-06-15 09:22:24 +00:00
case 'BOUNCER_LETSENCRYPT' :
2024-02-08 15:48:07 +00:00
$bouncerTarget -> setLetsEncrypt ( in_array ( strtolower ( $envVal ), [ 'yes' , 'true' ], true ));
break ;
case 'BOUNCER_CERT' :
$bouncerTarget -> setCustomCert ( $envVal );
$this -> logger -> info ( '{label}: Custom cert specified' , [ 'emoji' => Emoji :: locked (), 'label' => $bouncerTarget -> getLabel ()]);
break ;
case 'BOUNCER_CERT_KEY' :
$bouncerTarget -> setCustomCertKey ( $envVal );
2022-06-15 09:22:24 +00:00
break ;
case 'BOUNCER_TARGET_PORT' :
2024-02-08 15:48:07 +00:00
$bouncerTarget -> setPort ( intval ( $envVal ));
// $this->logger->info('{label}: Target port set to {port}.', ['emoji' => Emoji::ship(), 'label' => $bouncerTarget->getLabel(), 'port' => $bouncerTarget->getPort(),]);
2022-06-15 09:22:24 +00:00
break ;
case 'BOUNCER_ALLOW_NON_SSL' :
2024-02-08 15:48:07 +00:00
$bouncerTarget -> setAllowNonSSL ( in_array ( strtolower ( $envVal ), [ 'yes' , 'true' ], true ));
2022-06-15 09:22:24 +00:00
break ;
case 'BOUNCER_ALLOW_WEBSOCKETS' :
2024-02-08 15:48:07 +00:00
$bouncerTarget -> setAllowWebsocketSupport ( in_array ( strtolower ( $envVal ), [ 'yes' , 'true' ], true ));
2022-06-15 09:22:24 +00:00
2022-06-15 09:34:07 +00:00
break ;
case 'BOUNCER_ALLOW_LARGE_PAYLOADS' :
2024-02-08 15:48:07 +00:00
$bouncerTarget -> setAllowLargePayloads ( in_array ( strtolower ( $envVal ), [ 'yes' , 'true' ], true ));
2022-06-15 09:34:07 +00:00
break ;
2023-01-09 14:57:00 +00:00
case 'BOUNCER_PROXY_TIMEOUT_SECONDS' :
2024-02-08 15:48:07 +00:00
$bouncerTarget -> setProxyTimeoutSeconds ( is_numeric ( $envVal ) ? intval ( $envVal ) : null );
2023-01-09 14:57:00 +00:00
2024-02-26 14:50:50 +00:00
break ;
case 'BOUNCER_CUSTOM_NGINX_CONFIG' :
// If envval is base64 encoded, decode it first
2024-02-26 15:12:39 +00:00
if ( preg_match ( '/^[a-zA-Z0-9\/\r\n+]*={0,2}$/' , $envVal )) {
2024-02-26 14:50:50 +00:00
$envVal = base64_decode ( $envVal );
}
$this -> logger -> info ( 'Custom nginx config for {label} is provided.' , [ 'emoji' => Emoji :: artistPalette (), 'label' => $bouncerTarget -> getLabel ()]);
$bouncerTarget -> setCustomNginxConfig ( $envVal );
2022-06-15 09:22:24 +00:00
break ;
}
}
return $bouncerTarget ;
}
2024-01-05 14:47:37 +00:00
private function dockerGetContainers () : array
{
2024-01-25 13:01:25 +00:00
return json_decode ( $this -> docker -> request ( 'GET' , 'containers/json' ) -> getBody () -> getContents (), true );
2024-01-05 14:47:37 +00:00
}
private function dockerGetContainer ( string $id ) : array
{
2024-01-25 13:01:25 +00:00
return json_decode ( $this -> docker -> request ( 'GET' , " containers/ { $id } /json " ) -> getBody () -> getContents (), true );
2024-01-05 14:47:37 +00:00
}
2024-01-05 18:51:03 +00:00
private function dockerEnvFilter ( ? array $envs ) : array
{
if ( $envs === null ) {
return [];
}
2024-01-06 10:33:47 +00:00
$envs = array_filter ( array_map ( function ( $env ) {
2024-01-05 18:51:03 +00:00
if ( stripos ( $env , '=' ) !== false ) {
[ $envKey , $envVal ] = explode ( '=' , $env , 2 );
if ( strlen ( $envVal ) > 65 ) {
return sprintf ( '%s=CRC32(%s)' , $envKey , crc32 ( $envVal ));
}
return sprintf ( '%s=%s' , $envKey , $envVal );
}
return $env ;
}, $envs ));
2024-01-06 10:33:47 +00:00
sort ( $envs );
2024-01-08 00:35:15 +00:00
2024-01-06 10:33:47 +00:00
return $envs ;
2024-01-05 18:51:03 +00:00
}
2021-06-06 15:38:46 +00:00
/**
* Returns true when something has changed .
*
2024-01-25 13:01:25 +00:00
* @ throws GuzzleException
2021-06-06 15:38:46 +00:00
*/
private function stateHasChanged () : bool
{
2024-01-05 17:15:51 +00:00
$isTainted = false ;
if ( $this -> lastUpdateEpoch === null ) {
$isTainted = true ;
} elseif ( $this -> forcedUpdateIntervalSeconds > 0 && $this -> lastUpdateEpoch <= time () - $this -> forcedUpdateIntervalSeconds ) {
2024-02-08 15:48:07 +00:00
$this -> logger -> warning ( 'Forced update interval of {interval_seconds} seconds has been reached, forcing update.' , [ 'emoji' => Emoji :: watch (), 'interval_seconds' => $this -> forcedUpdateIntervalSeconds ]);
2024-01-05 17:15:51 +00:00
$isTainted = true ;
} elseif ( $this -> previousContainerState === []) {
2024-02-08 15:48:07 +00:00
$this -> logger -> warning ( 'Initial state has not been set, forcing update.' , [ 'emoji' => Emoji :: watch ()]);
2024-01-05 17:15:51 +00:00
$isTainted = true ;
} elseif ( $this -> previousSwarmState === []) {
2024-02-08 15:48:07 +00:00
$this -> logger -> warning ( 'Initial swarm state has not been set, forcing update.' , [ 'emoji' => Emoji :: watch ()]);
2024-01-05 17:15:51 +00:00
$isTainted = true ;
2022-08-09 00:28:22 +00:00
}
2022-05-05 16:15:11 +00:00
// Standard Containers
2024-01-05 14:47:37 +00:00
$newContainerState = [];
2024-01-05 17:16:21 +00:00
$containers = $this -> dockerGetContainers ();
2021-06-06 15:38:46 +00:00
foreach ( $containers as $container ) {
2024-01-05 17:16:21 +00:00
$inspect = $this -> dockerGetContainer ( $container [ 'Id' ]);
$name = ltrim ( $inspect [ 'Name' ], '/' );
2024-01-08 17:57:05 +00:00
$env = $inspect [ 'Config' ][ 'Env' ] ? ? [];
2024-01-10 16:31:07 +00:00
// if (!$this->dockerEnvHas('BOUNCER_DOMAIN', $env)) {
// continue;
// }
2024-01-08 17:57:05 +00:00
2024-01-05 17:15:51 +00:00
$newContainerState [ $name ] = [
2024-01-05 17:16:21 +00:00
'name' => $name ,
2024-01-05 17:15:51 +00:00
'created' => $inspect [ 'Created' ],
2024-01-05 17:16:21 +00:00
'image' => $inspect [ 'Image' ],
'status' => $inspect [ 'State' ][ 'Status' ],
2024-01-08 17:57:05 +00:00
'env' => $this -> dockerEnvFilter ( $env ),
2024-01-05 14:47:37 +00:00
];
2024-01-05 18:51:03 +00:00
if ( is_array ( $newContainerState [ $name ][ 'env' ])) {
sort ( $newContainerState [ $name ][ 'env' ]);
}
2024-01-05 14:47:37 +00:00
}
ksort ( $newContainerState );
// Calculate Container State Hash
2024-01-05 17:15:51 +00:00
$containerStateDiff = $this -> diff ( $this -> previousContainerState , $newContainerState );
if ( ! $isTainted && ! empty ( $containerStateDiff )) {
2024-02-08 15:48:07 +00:00
if ( $this -> settings -> if ( 'logger/show_state_deltas' )) {
2024-05-16 11:22:34 +00:00
$this -> logger -> warning ( 'Container state has changed' , [ 'emoji' => Emoji :: warning () . ' Bouncer.php' ]);
2024-01-27 02:43:32 +00:00
echo $containerStateDiff ;
}
2024-01-05 17:15:51 +00:00
$isTainted = true ;
2021-06-06 15:38:46 +00:00
}
2024-01-05 17:15:51 +00:00
$this -> previousContainerState = $newContainerState ;
2022-05-05 16:15:11 +00:00
// Swarm Services
2024-01-05 14:47:37 +00:00
$newSwarmState = [];
2022-06-29 13:37:35 +00:00
if ( $this -> isSwarmMode ()) {
2024-01-25 13:01:25 +00:00
$services = json_decode ( $this -> docker -> request ( 'GET' , 'services' ) -> getBody () -> getContents (), true );
2022-06-29 13:37:35 +00:00
if ( isset ( $services [ 'message' ])) {
2024-05-16 11:22:34 +00:00
$this -> logger -> warning ( 'Something happened while interrogating services.. This node is not a swarm node, cannot have services: {message}' , [ 'emoji' => Emoji :: warning () . ' Bouncer.php' , 'message' => $services [ 'message' ]]);
2022-06-29 13:37:35 +00:00
} else {
foreach ( $services as $service ) {
2024-01-05 18:51:03 +00:00
$name = $service [ 'Spec' ][ 'Name' ];
2024-01-08 17:57:05 +00:00
$env = $service [ 'Spec' ][ 'TaskTemplate' ][ 'ContainerSpec' ][ 'Env' ] ? ? [];
2024-01-10 16:31:07 +00:00
// if (!$this->dockerEnvHas('BOUNCER_DOMAIN', $env)) {
// continue;
// }
2024-01-05 18:51:03 +00:00
$newSwarmState [ $name ] = [
'id' => $service [ 'ID' ],
'mode' => isset ( $service [ 'Spec' ][ 'Mode' ][ 'Replicated' ]) ?
sprintf ( 'replicated:%d' , $service [ 'Spec' ][ 'Mode' ][ 'Replicated' ][ 'Replicas' ]) :
( isset ( $service [ 'Spec' ][ 'Mode' ][ 'Global' ]) ? 'global' : 'none' ),
'created' => $service [ 'CreatedAt' ],
'image' => $service [ 'Spec' ][ 'TaskTemplate' ][ 'ContainerSpec' ][ 'Image' ],
'versionIndex' => $service [ 'Version' ][ 'Index' ],
'updateStatus' => $service [ 'UpdateStatus' ][ 'State' ] ? ? 'unknown' ,
'env' => $this -> dockerEnvFilter ( $env ),
2024-01-05 14:47:37 +00:00
];
2022-06-29 13:37:35 +00:00
}
2022-05-05 16:15:11 +00:00
}
}
2024-01-05 14:47:37 +00:00
ksort ( $newSwarmState );
2021-06-06 15:38:46 +00:00
2024-01-05 14:47:37 +00:00
// Calculate Swarm State Hash, if applicable
2024-01-05 17:15:51 +00:00
$swarmStateDiff = $this -> diff ( $this -> previousSwarmState , $newSwarmState );
if ( $this -> isSwarmMode () && ! $isTainted && ! empty ( $swarmStateDiff )) {
2024-02-08 15:48:07 +00:00
if ( $this -> settings -> if ( 'logger/show_state_deltas' )) {
2024-05-16 11:22:34 +00:00
$this -> logger -> warning ( 'Swarm state has changed' , [ 'emoji' => Emoji :: warning () . ' Bouncer.php' ]);
2024-01-27 02:43:32 +00:00
echo $swarmStateDiff ;
}
2024-01-05 17:15:51 +00:00
$isTainted = true ;
2021-06-06 15:38:46 +00:00
}
2024-01-05 17:15:51 +00:00
$this -> previousSwarmState = $newSwarmState ;
2021-06-06 15:38:46 +00:00
2024-01-05 17:15:51 +00:00
return $isTainted ;
}
private function diff ( $a , $b )
{
2024-01-25 13:01:25 +00:00
return ( new \Diff (
2024-01-05 17:15:51 +00:00
explode (
" \n " ,
Yaml :: dump ( input : $a , inline : 5 , indent : 2 )
),
explode (
" \n " ,
Yaml :: dump ( input : $b , inline : 5 , indent : 2 )
)
2024-01-25 13:01:25 +00:00
)) -> render ( new \Diff_Renderer_Text_Unified ());
2021-06-06 15:38:46 +00:00
}
private function runLoop () : void
{
if ( $this -> s3Enabled ()) {
$this -> getCertificatesFromS3 ();
}
2022-06-29 13:37:35 +00:00
try {
2024-01-25 13:01:25 +00:00
$determineSwarmMode = json_decode ( $this -> docker -> request ( 'GET' , 'swarm' ) -> getBody () -> getContents (), true );
2022-06-29 13:37:35 +00:00
$this -> setSwarmMode ( ! isset ( $determineSwarmMode [ 'message' ]));
2023-01-09 14:57:00 +00:00
} catch ( ServerException $exception ) {
2022-06-29 13:37:35 +00:00
$this -> setSwarmMode ( false );
2023-01-09 14:57:00 +00:00
} catch ( ConnectException $exception ) {
2024-05-16 11:22:34 +00:00
$this -> logger -> critical ( 'Unable to connect to docker socket!' , [ 'emoji' => Emoji :: warning () . ' Bouncer.php' ]);
2023-01-09 14:57:00 +00:00
$this -> logger -> critical ( $exception -> getMessage ());
exit ( 1 );
2022-06-29 13:37:35 +00:00
}
2024-02-08 15:48:07 +00:00
$this -> logger -> debug ( ' > Swarm mode is {enabled}.' , [ 'emoji' => Emoji :: honeybee (), 'enabled' => $this -> isSwarmMode () ? 'enabled' : 'disabled' ]);
2022-08-09 01:12:09 +00:00
2022-08-09 02:09:25 +00:00
$targets = array_values (
array_merge (
$this -> findContainersContainerMode (),
$this -> isSwarmMode () ? $this -> findContainersSwarmMode () : []
)
2022-08-09 01:12:09 +00:00
);
2022-05-05 14:42:53 +00:00
2024-01-05 17:15:51 +00:00
// Use some bs to sort the targets by domain from right to left.
$sortedTargets = [];
foreach ( $targets as $target ) {
$sortedTargets [ strrev ( $target -> getName ())] = $target ;
}
ksort ( $sortedTargets );
$targets = array_values ( $sortedTargets );
2024-01-25 15:54:34 +00:00
// Re-generate nginx configs
2024-01-25 15:59:48 +00:00
$this -> logger -> info ( 'Found {num_services} services with BOUNCER_DOMAIN set' , [ 'emoji' => Emoji :: magnifyingGlassTiltedLeft (), 'num_services' => count ( $targets )]);
2023-01-09 14:57:00 +00:00
$this -> generateNginxConfigs ( $targets );
2021-06-06 15:38:46 +00:00
$this -> generateLetsEncryptCerts ( $targets );
if ( $this -> s3Enabled ()) {
$this -> writeCertificatesToS3 ();
}
2024-04-30 14:33:23 +00:00
// if any of the targets has requiresForcedScanning set to true, we need to force an update
if ( array_reduce ( $targets , fn ( $carry , $target ) => $carry || $target -> requiresForcedScanning (), false )) {
$this -> logger -> warning ( 'Forcing an update in 5 seconds because one or more targets require it.' , [ 'emoji' => Emoji :: warning ()]);
sleep ( 5 );
return ;
}
// Wait for next change
2021-06-06 15:38:46 +00:00
$this -> waitUntilContainerChange ();
}
private function waitUntilContainerChange () : void
{
while ( $this -> stateHasChanged () === false ) {
sleep ( 5 );
}
2022-08-09 00:28:22 +00:00
$this -> lastUpdateEpoch = time ();
2021-06-06 15:38:46 +00:00
}
private function s3Enabled () : bool
{
return $this -> certificateStoreRemote instanceof Filesystem ;
}
private function getCertificatesFromS3 () : void
{
$this -> logger -> info ( sprintf ( '%s Downloading Certificates from S3' , Emoji :: CHARACTER_DOWN_ARROW ));
foreach ( $this -> certificateStoreRemote -> listContents ( '/' , true ) as $file ) {
/** @var FileAttributes $file */
if ( $file -> isFile ()) {
$localPath = " archive/ { $file -> path () } " ;
2024-01-05 14:47:37 +00:00
if ( $file -> fileSize () == 0 ) {
$this -> logger -> warning ( sprintf ( ' > Downloading %s to %s was skipped, because it was empty' , $file -> path (), $localPath ));
2023-01-15 02:37:12 +00:00
continue ;
}
2024-01-05 14:47:37 +00:00
$this -> logger -> debug ( sprintf ( ' > Downloading %s to %s (%d bytes)' , $file -> path (), $localPath , $file -> fileSize ()));
2021-06-06 15:38:46 +00:00
$this -> certificateStoreLocal -> writeStream ( $localPath , $this -> certificateStoreRemote -> readStream ( $file -> path ()));
2024-01-05 14:47:37 +00:00
if ( $this -> certificateStoreLocal -> fileSize ( $localPath ) == $this -> certificateStoreRemote -> fileSize ( $file -> path ())) {
$this -> logger -> debug ( sprintf ( ' > Filesize for %s matches %s on remote (%d bytes)' , $localPath , $file -> path (), $this -> certificateStoreLocal -> fileSize ( $localPath )));
} else {
$this -> logger -> critical ( sprintf ( ' > Filesize for %s DOES NOT MATCH %s on remote (%d != %d bytes)' , $localPath , $file -> path (), $this -> certificateStoreLocal -> fileSize ( $localPath ), $this -> certificateStoreRemote -> fileSize ( $file -> path ())));
2023-02-07 14:26:44 +00:00
}
2021-06-06 15:38:46 +00:00
$this -> fileHashes [ $localPath ] = sha1 ( $this -> certificateStoreLocal -> read ( $localPath ));
}
}
// Copy certs into /live because certbot is a pain.
foreach ( $this -> certificateStoreLocal -> listContents ( '/archive' , true ) as $newLocalCert ) {
/** @var FileAttributes $newLocalCert */
if ( $newLocalCert -> isFile () && pathinfo ( $newLocalCert -> path (), PATHINFO_EXTENSION ) == 'pem' ) {
$livePath = str_replace ( 'archive/' , 'live/' , $newLocalCert -> path ());
2021-06-06 22:08:49 +00:00
// Stupid dirty hack bullshit reee
2022-06-15 09:22:24 +00:00
for ( $i = 1 ; $i <= 9 ; ++ $i ) {
$livePath = str_replace ( " { $i } .pem " , '.pem' , $livePath );
2021-06-06 22:08:49 +00:00
}
2024-01-05 14:47:37 +00:00
$this -> logger -> debug ( sprintf ( ' > Mirroring %s to %s (%d bytes)' , $newLocalCert -> path (), $livePath , $newLocalCert -> fileSize ()));
2021-06-06 15:38:46 +00:00
$this -> certificateStoreLocal -> writeStream ( $livePath , $this -> certificateStoreLocal -> readStream ( $newLocalCert -> path ()));
}
}
}
private function fileChanged ( string $localPath )
{
if ( ! isset ( $this -> fileHashes [ $localPath ])) {
return true ;
}
if ( sha1 ( $this -> certificateStoreLocal -> read ( $localPath )) != $this -> fileHashes [ $localPath ]) {
return true ;
}
return false ;
}
private function writeCertificatesToS3 () : void
{
2024-02-08 15:48:07 +00:00
$this -> logger -> info ( 'Uploading Certificates to S3' , [ 'emoji' => Emoji :: CHARACTER_UP_ARROW ]);
2021-06-06 15:38:46 +00:00
foreach ( $this -> certificateStoreLocal -> listContents ( '/archive' , true ) as $file ) {
/** @var FileAttributes $file */
if ( $file -> isFile ()) {
$remotePath = str_replace ( 'archive/' , '' , $file -> path ());
2024-01-05 14:47:37 +00:00
if ( $file -> fileSize () == 0 ) {
2024-01-25 13:01:25 +00:00
$this -> logger -> warning ( ' > Skipping uploading {file}, file is garbage (empty).' , [ 'file' => $file -> path ()]);
2023-01-15 02:37:12 +00:00
} elseif ( ! $this -> certificateStoreRemote -> fileExists ( $remotePath ) || $this -> fileChanged ( $file -> path ())) {
2024-01-25 13:01:25 +00:00
$this -> logger -> debug ( ' > Uploading {file} ({bytes} bytes)' , [ 'file' => $file -> path (), 'bytes' => $file -> fileSize ()]);
2023-01-15 02:37:12 +00:00
$this -> certificateStoreRemote -> write ( $remotePath , $this -> certificateStoreLocal -> read ( $file -> path ()));
} else {
2024-01-25 13:01:25 +00:00
$this -> logger -> debug ( ' > Skipping uploading {file}, file not changed.' , [ 'file' => $file -> path ()]);
2021-06-06 15:38:46 +00:00
}
}
}
}
2023-01-09 14:57:00 +00:00
/**
2024-01-25 13:01:25 +00:00
* @ param $targets Target []
2023-01-09 14:57:00 +00:00
*/
2024-01-08 00:35:15 +00:00
private function generateNginxConfigs ( array $targets ) : void
2023-01-09 14:57:00 +00:00
{
2024-01-25 14:37:54 +00:00
$changedTargets = [];
2023-01-09 14:57:00 +00:00
foreach ( $targets as $target ) {
2024-01-25 14:37:54 +00:00
if ( $this -> generateNginxConfig ( $target )) {
2024-01-25 15:02:12 +00:00
$changedTargets [ strrev ( $target -> getName ())] = $target ;
2023-01-09 14:57:00 +00:00
}
}
2024-02-08 15:48:07 +00:00
// @var Target[] $changedTargets
2024-01-25 15:02:12 +00:00
ksort ( $changedTargets );
$changedTargets = array_values ( $changedTargets );
2024-01-25 15:18:06 +00:00
2024-01-25 14:37:54 +00:00
if ( count ( $changedTargets ) <= $this -> getMaximumNginxConfigCreationNotices ()) {
2024-02-08 15:48:07 +00:00
/** @var Target $target */
2024-01-25 14:37:54 +00:00
foreach ( $changedTargets as $target ) {
2024-01-25 15:18:06 +00:00
$context = [
'label' => $target -> getLabel (),
'domain' => $target -> getPresentationdomain (),
2024-01-25 15:59:36 +00:00
'file' => $target -> getNginxConfigFileName (),
2024-01-25 15:18:06 +00:00
'config_dir' => Bouncer :: FILESYSTEM_CONFIG_DIR ,
];
2024-05-16 11:22:34 +00:00
$this -> logger -> info ( 'Created {label}' , $context + [ 'emoji' => Emoji :: pencil () . ' Bouncer.php' ]);
$this -> logger -> debug ( ' -> {config_dir}/{file}' , $context + [ 'emoji' => Emoji :: pencil () . ' Bouncer.php' ]);
$this -> logger -> debug ( ' -> {domain}' , $context + [ 'emoji' => Emoji :: pencil () . ' Bouncer.php' ]);
2024-04-30 14:28:27 +00:00
$this -> logger -> critical ( '{label} cert type is {cert_type}' , $context + [ 'emoji' => Emoji :: catFace (), 'cert_type' => $target -> getTypeCertInUse () -> name ]);
2024-01-25 14:37:54 +00:00
}
} else {
2024-05-16 11:22:34 +00:00
$this -> logger -> info ( 'More than {num_max} Nginx configs generated.. Too many to show them all!' , [ 'emoji' => Emoji :: pencil () . ' Bouncer.php' , 'num_max' => $this -> getMaximumNginxConfigCreationNotices ()]);
2023-01-09 14:57:00 +00:00
}
2024-05-16 11:22:34 +00:00
$this -> logger -> info ( 'Updated {num_created} Nginx configs, {num_changed} changed..' , [ 'emoji' => Emoji :: pencil () . ' Bouncer.php' , 'num_created' => count ( $targets ), 'num_changed' => count ( $changedTargets )]);
2024-01-25 15:54:34 +00:00
$this -> pruneNonExistentConfigs ( $targets );
}
/**
* @ param $targets Target []
2024-02-08 15:48:07 +00:00
*
* @ throws FilesystemException
2024-01-25 15:54:34 +00:00
*/
protected function pruneNonExistentConfigs ( array $targets ) : void
{
$expectedFiles = [
'default.conf' ,
];
foreach ( $targets as $target ) {
$expectedFiles = array_merge ( $expectedFiles , $target -> getExpectedFiles ());
}
foreach ( $this -> configFilesystem -> listContents ( '/' ) as $file ) {
if ( ! in_array ( $file [ 'path' ], $expectedFiles )) {
2024-02-08 15:48:07 +00:00
$this -> logger -> info ( 'Removing {file}' , [ 'emoji' => Emoji :: wastebasket (), 'file' => $file [ 'path' ]]);
2024-01-25 15:54:34 +00:00
$this -> configFilesystem -> delete ( $file [ 'path' ]);
}
}
2023-01-09 14:57:00 +00:00
}
2024-02-08 15:48:07 +00:00
/**
* @ throws FilesystemException
* @ throws LoaderError
* @ throws RuntimeError
* @ throws SyntaxError
*/
2024-01-25 14:37:54 +00:00
private function generateNginxConfig ( Target $target ) : bool
2021-06-06 15:38:46 +00:00
{
2024-02-26 14:50:50 +00:00
$configData = $target -> hasCustomNginxConfig () ? $target -> getCustomNginxConfig () : $this -> twig -> render ( 'NginxTemplate.twig' , $target -> __toArray ());
2024-01-25 14:37:54 +00:00
$changed = false ;
2024-01-25 15:54:34 +00:00
$configFileHash = $this -> configFilesystem -> fileExists ( $target -> getNginxConfigFileName ()) ? sha1 ( $this -> configFilesystem -> read ( $target -> getNginxConfigFileName ())) : null ;
2024-01-25 14:37:54 +00:00
if ( sha1 ( $configData ) != $configFileHash ) {
2024-01-25 15:54:34 +00:00
$this -> configFilesystem -> write ( $target -> getNginxConfigFileName (), $configData );
2024-01-25 14:37:54 +00:00
$changed = true ;
}
2024-02-08 15:48:07 +00:00
if ( $target -> isUseCustomCert ()) {
$this -> configFilesystem -> write ( $target -> getCustomCertPath (), $target -> getCustomCert ());
$this -> configFilesystem -> write ( $target -> getCustomCertKeyPath (), $target -> getCustomCertKey ());
}
2024-01-05 14:47:37 +00:00
if ( $target -> hasAuth ()) {
2024-01-25 15:54:34 +00:00
$authFileHash = $this -> configFilesystem -> fileExists ( $target -> getBasicAuthFileName ()) ? $this -> configFilesystem -> read ( $target -> getBasicAuthHashFileName ()) : null ;
2024-01-25 15:02:53 +00:00
if ( $target -> getAuthHash () != $authFileHash ) {
2024-01-25 15:54:34 +00:00
$this -> configFilesystem -> write ( $target -> getBasicAuthHashFileName (), $target -> getAuthHash ());
$this -> configFilesystem -> write ( $target -> getBasicAuthFileName (), $target -> getBasicAuthFileData ());
2024-01-25 14:37:54 +00:00
$changed = true ;
}
2023-01-09 14:57:00 +00:00
}
2024-01-25 14:37:54 +00:00
return $changed ;
2021-06-06 15:38:46 +00:00
}
/**
2024-01-25 13:01:25 +00:00
* @ param Target [] $targets
2021-06-06 15:38:46 +00:00
*
2024-01-08 00:35:15 +00:00
* @ throws FilesystemException
2021-06-06 15:38:46 +00:00
*/
2024-01-08 00:35:15 +00:00
private function generateLetsEncryptCerts ( array $targets ) : void
2021-06-06 15:38:46 +00:00
{
foreach ( $targets as $target ) {
if ( ! $target -> isLetsEncrypt ()) {
continue ;
}
$testAgeFile = " /archive/ { $target -> getName () } /fullchain1.pem " ;
if ( $this -> certificateStoreLocal -> fileExists ( $testAgeFile )) {
2023-02-07 14:26:44 +00:00
$dubious = false ;
2024-01-05 14:47:37 +00:00
if ( $this -> certificateStoreLocal -> fileSize ( $testAgeFile ) == 0 ) {
2023-02-07 14:26:44 +00:00
// File is empty, check its age instead.
$timeRemainingSeconds = $this -> certificateStoreLocal -> lastModified ( $testAgeFile ) - time ();
2024-01-05 17:16:21 +00:00
$dubious = true ;
2024-01-05 14:47:37 +00:00
} else {
2024-01-05 17:16:21 +00:00
$ssl = openssl_x509_parse ( $this -> certificateStoreLocal -> read ( $testAgeFile ));
2023-02-07 14:26:44 +00:00
$timeRemainingSeconds = $ssl [ 'validTo_time_t' ] - time ();
}
2021-06-06 15:38:46 +00:00
if ( $timeRemainingSeconds > 2592000 ) {
2024-01-25 13:01:25 +00:00
$this -> logger -> info (
2024-01-25 15:59:48 +00:00
'Skipping {target_name}, certificate is {validity} for {duration_days} days' ,
2024-01-25 13:01:25 +00:00
[
'emoji' => Emoji :: CHARACTER_PARTYING_FACE ,
'target_name' => $target -> getName (),
'validity' => $dubious ? 'dubiously good' : 'still good' ,
'duration_days' => round ( $timeRemainingSeconds / 86400 ),
]
);
2021-06-06 15:38:46 +00:00
2021-06-06 22:08:49 +00:00
$target -> setUseTemporaryCert ( false );
$this -> generateNginxConfig ( $target );
2022-06-15 09:22:24 +00:00
2021-06-06 15:38:46 +00:00
continue ;
}
}
2023-01-11 17:13:24 +00:00
// Start running shell commands...
2021-06-06 15:38:46 +00:00
$shell = new Exec ();
2023-01-11 17:13:24 +00:00
// Disable nginx tweaks
2024-01-25 15:59:48 +00:00
$this -> logger -> debug ( 'Moving nginx tweak file out of the way..' , [ 'emoji' => Emoji :: rightArrow ()]);
2024-01-05 14:47:37 +00:00
$disableNginxTweaksCommand = ( new CommandBuilder ( 'mv' ))
-> addSubCommand ( '/etc/nginx/conf.d/tweak.conf' )
-> addSubCommand ( '/etc/nginx/conf.d/tweak.disabled' )
;
2023-01-11 17:13:24 +00:00
$shell -> run ( $disableNginxTweaksCommand );
// Generate letsencrypt cert
2021-06-06 15:38:46 +00:00
$command = new CommandBuilder ( '/usr/bin/certbot' );
$command -> addSubCommand ( 'certonly' );
$command -> addArgument ( 'nginx' );
if ( $this -> environment [ 'BOUNCER_LETSENCRYPT_MODE' ] != 'production' ) {
$command -> addArgument ( 'test-cert' );
}
$command -> addFlag ( 'd' , implode ( ',' , $target -> getDomains ()));
$command -> addFlag ( 'n' );
$command -> addFlag ( 'm' , $this -> environment [ 'BOUNCER_LETSENCRYPT_EMAIL' ]);
$command -> addArgument ( 'agree-tos' );
2024-05-16 11:22:34 +00:00
$this -> logger -> info ( 'Generating letsencrypt for {target_name} - {command}' , [ 'emoji' => Emoji :: pencil () . ' Bouncer.php' , 'target_name' => $target -> getName (), 'command' => $command -> __toString ()]);
2021-06-06 15:38:46 +00:00
$shell -> run ( $command );
if ( $shell -> getReturnValue () == 0 ) {
2024-01-25 15:59:48 +00:00
$this -> logger -> info ( 'Generating successful' , [ 'emoji' => Emoji :: partyPopper ()]);
2021-06-06 15:38:46 +00:00
} else {
2024-05-16 11:22:34 +00:00
$this -> logger -> critical ( 'Generating failed!' , [ 'emoji' => Emoji :: warning () . ' Bouncer.php' ]);
2021-06-06 15:38:46 +00:00
}
2023-01-11 17:13:24 +00:00
// Re-enable nginx tweaks
2024-01-25 15:59:48 +00:00
$this -> logger -> debug ( 'Moving nginx tweak file back in place..' , [ 'emoji' => Emoji :: leftArrow ()]);
2024-01-05 14:47:37 +00:00
$disableNginxTweaksCommand = ( new CommandBuilder ( 'mv' ))
-> addSubCommand ( '/etc/nginx/conf.d/tweak.disabled' )
-> addSubCommand ( '/etc/nginx/conf.d/tweak.conf' )
;
2023-01-11 17:13:24 +00:00
$shell -> run ( $disableNginxTweaksCommand );
2021-06-06 15:38:46 +00:00
$target -> setUseTemporaryCert ( false );
$this -> generateNginxConfig ( $target );
}
$this -> restartNginx ();
}
private function restartNginx () : void
{
2024-01-05 17:16:21 +00:00
$shell = new Exec ();
2021-06-06 15:38:46 +00:00
$command = new CommandBuilder ( '/usr/sbin/nginx' );
$command -> addFlag ( 's' , 'reload' );
2024-05-16 11:22:34 +00:00
$this -> logger -> info ( 'Restarting nginx' , [ 'emoji' => Emoji :: timerClock () . ' Bouncer.php' ]);
2024-04-03 20:36:41 +00:00
$nginxRestartOutput = $shell -> run ( $command );
$this -> logger -> debug ( 'Nginx restarted {restart_output}' , [ 'restart_output' => $nginxRestartOutput , 'emoji' => Emoji :: partyPopper ()]);
2021-06-06 15:38:46 +00:00
}
}