Upstreaming changes from downstream project

This commit is contained in:
Greyscale 2022-08-09 01:28:22 +01:00
parent d62725db5f
commit 9d437e266f
No known key found for this signature in database
GPG key ID: CEC4822A25BA80C5
7 changed files with 304 additions and 109 deletions

7
bouncer/.gitignore vendored
View file

@ -1,3 +1,4 @@
/docker-compose.override.yml
/vendor
/.php-cs-fixer.cache
.idea
docker-compose.override.yml
vendor
.php-cs-fixer.cache

View file

@ -49,11 +49,13 @@ COPY composer.* /app/
COPY public /app/public
RUN composer install && \
chmod +x /app/bouncer && \
mkdir -p /var/log/bouncer
mkdir -p /var/log/bouncer && \
rm /etc/nginx/sites-enabled/default && \
cp /etc/nginx/sites-available/default /etc/nginx/sites-enabled/default
FROM benzine/php:nginx-8.0 as test-app-a
FROM benzine/php:nginx-8.1 as test-app-a
COPY ./test/public-web-a /app/public
FROM benzine/php:nginx-8.0 as test-app-b
FROM benzine/php:nginx-8.1 as test-app-b
COPY ./test/public-web-b /app/public

View file

@ -12,6 +12,9 @@ server {
{% if useTemporaryCert %}
ssl_certificate /certs/example.crt;
ssl_certificate_key /certs/example.key;
{% elseif useGlobalCert %}
ssl_certificate /certs/global.crt;
ssl_certificate_key /certs/global.key;
{% else %}
ssl_certificate /etc/letsencrypt/live/{{ name }}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/{{ name }}/privkey.pem;

48
bouncer/Readme.md Normal file
View file

@ -0,0 +1,48 @@
# Automatic Swarm Nginx Loadbalancer
## Environment variables
This container has its own environment variables, AS WELL AS scanning for some environment variables associated with your services.
These should not be confused.
### Load balancer Configuration
#### Main configuration
| Key | Default | Options | Behaviour |
|----------------------------------------|---------|---------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| DOCKER_HOST | false | | Define a http endpoint representing your docker socket. If this is null, it connects to /var/lib/docker.sock |
| GLOBAL_CERT | false | Contents of an ssl certificate | If you want to provide a single cert for all endpoints, perhaps with a catch-all that may be later overriden, you can provide the whole contents of a certificates file here. |
| GLOBAL_CERT_KEY | false | Contents of an ssl certificates private key | The private key related to GLOBAL CERT. These must be provided in tandem. |
| BOUNCER_FORCED_UPDATE_INTERVAL_SECONDS | false | positive numbers | To force the bouncer to update on a schedule even if no changes are detected, measured in seconds |
#### For using with lets encrypt
| Key | Default | Options | Behaviour |
|---------------------------|-----------|---------------------------|--------------------------------------------------------------------------------------|
| BOUNCER_LETSENCRYPT_MODE | 'staging' | 'staging' or 'production' | Determine if this is going to connect to a production or staging Lets Encrypt server |
| BOUNCER_LETSENCRYPT_EMAIL | | 'bob@example.com' | Email address to associate with lets encrypt |
#### For using S3 for generated cert synchronisation with Lets Encrypt
| Key | Default | Options | Behaviour |
|------------------------------------|---------|-----------------|---------------------------------------------------------------------------------------|
| BOUNCER_S3_BUCKET | false | | enable S3 behaviour to store lets-encrypt generated certs |
| BOUNCER_S3_ENDPOINT | false | | define s3 endpoint to override default AWS s3 implementation, for example, with minio |
| BOUNCER_S3_KEY_ID | false | | S3 API Key ID | |
| BOUNCER_s3_KEY_SECRET | false | | S3 API Key Secret |
| BOUNCER_S3_REGION | false | | S3 API Region |
| BOUNCER_S3_USE_PATH_STYLE_ENDPOINT | false | `true or false` | Needed for minio |
| BOUNCER_S3_PREFIX | false | | Prefix file path in s3 bucket |
### Served Instance Configuration
These environment variables need to be applied to the CONSUMING SERVICE and not the loadbalancer container itself.
| Key | Example | Behaviour |
|--------------------------------|-------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------|
| BOUNCER_DOMAIN | "a.example.com" | The domain that should be directed to this container |
| BOUNCER_LETSENCRYPT | Values are "yes" or "true", anything else is false | To enable, or disable Lets Encrypt service for this hostname |
| BOUNCER_TARGET_PORT | 9000 | Explicitly define the port you want to hit the service on, in case of ambiguity |
| BOUNCER_ALLOW_NON_SSL | Defaults to enabled. Values are "yes" or "true", anything else is false | Should HTTP only traffic be allowed to hit this service? If disabled, http traffic is forwarded towards https |
| BOUNCER_ALLOW_WEBSOCKETS | Defaults to enabled. Values are "yes" or "true", anything else is false | Enable websocket behaviour |
| BOUNCER_ALLOW_LARGE_PAYLOADS | Defaults to disabled. | Allows overriding the default nginx payload size. Related to BOUNCER_MAX_PAYLOADS_MEGABYTES |
| BOUNCER_MAX_PAYLOADS_MEGABYTES | numbers | Size of max payload to allow, in megabytes. Requires BOUNCER_ALLOW_LARGE_PAYLOADS to be enabled |
## Security considerations
If you're putting this behind access control to the docker socket, it will need access to the /swarm /services and /containers endpoints of the docker api.

View file

@ -21,16 +21,22 @@ class BouncerTarget
{
private string $id;
private array $domains;
private string $ip;
private int $port = 80;
private string $endpointHostnameOrIp;
private ?int $port = null;
private bool $letsEncrypt = false;
private string $targetPath;
private bool $allowNonSSL = true;
private bool $useTemporaryCert = true;
private bool $useGlobalCert = false;
private bool $allowWebsocketSupport = true;
private bool $allowLargePayloads = false;
private int $maxPayloadSizeMegabytes = 256;
public function __construct(
private Logger $logger
) {
}
public function __toArray()
{
return [
@ -40,6 +46,7 @@ class BouncerTarget
'letsEncrypt' => $this->isLetsEncrypt(),
'targetPath' => $this->getTargetPath(),
'useTemporaryCert' => $this->isUseTemporaryCert(),
'useGlobalCert' => $this->isUseGlobalCert(),
'allowNonSSL' => $this->isAllowNonSSL(),
'allowWebsocketSupport' => $this->isAllowWebsocketSupport(),
'allowLargePayloads' => $this->isAllowLargePayloads(),
@ -59,6 +66,21 @@ class BouncerTarget
return $this;
}
public function isUseGlobalCert(): bool
{
return $this->useGlobalCert;
}
public function setUseGlobalCert(bool $useGlobalCert): BouncerTarget
{
$this->useGlobalCert = $useGlobalCert;
// Global cert overrides temporary certs.
$this->setUseTemporaryCert(false);
return $this;
}
public function isAllowWebsocketSupport(): bool
{
return $this->allowWebsocketSupport;
@ -149,23 +171,28 @@ class BouncerTarget
return $this;
}
public function getIp(): string
public function getEndpointHostnameOrIp(): string
{
return $this->ip;
return $this->endpointHostnameOrIp;
}
public function setIp(string $ip): BouncerTarget
public function setEndpointHostnameOrIp(string $endpointHostnameOrIp): BouncerTarget
{
$this->ip = $ip;
$this->endpointHostnameOrIp = $endpointHostnameOrIp;
return $this;
}
public function getPort(): int
public function getPort(): ?int
{
return $this->port;
}
public function isPortSet(): bool
{
return $this->port !== null;
}
public function setPort(int $port): BouncerTarget
{
$this->port = $port;
@ -189,6 +216,28 @@ class BouncerTarget
return $this;
}
public function isEndpointValid(): bool
{
// Is it just an IP?
if (filter_var($this->getEndpointHostnameOrIp(), FILTER_VALIDATE_IP)) {
// $this->logger->debug(sprintf('%s isEndpointValid: %s is a normal IP', Emoji::magnifyingGlassTiltedRight(), $this->getEndpointHostnameOrIp()));
return true;
}
// Is it a Hostname that resolves?
$resolved = gethostbyname($this->getEndpointHostnameOrIp());
if (filter_var($resolved, FILTER_VALIDATE_IP)) {
// $this->logger->debug(sprintf('%s isEndpointValid: %s is a hostname that resolves to a normal IP %s', Emoji::magnifyingGlassTiltedRight(), $this->getEndpointHostnameOrIp(), $resolved));
return true;
}
// $this->logger->debug(sprintf('%s isEndpointValid: %s is a hostname does not resolve', Emoji::magnifyingGlassTiltedRight(), $this->getEndpointHostnameOrIp()));
return false;
}
}
class Bouncer
@ -200,10 +249,14 @@ class Bouncer
private Filesystem $configFilesystem;
private Filesystem $certificateStoreLocal;
private ?Filesystem $certificateStoreRemote = null;
private Filesystem $providedCertificateStore;
private Logger $logger;
private string $instanceStateHash = '';
private array $fileHashes;
private bool $swarmMode = false;
private bool $useGlobalCert = false;
private int $forcedUpdateIntervalSeconds = 600;
private ?int $lastUpdateEpoch = null;
public function __construct()
{
@ -216,14 +269,13 @@ class Bouncer
$stdout->setFormatter(new ColoredLineFormatter(null, "%level_name%: %message% \n"));
$this->logger->pushHandler($stdout);
$this->client = new Guzzle(
[
'base_uri' => 'http://localhost',
'curl' => [
CURLOPT_UNIX_SOCKET_PATH => '/var/run/docker.sock',
],
]
);
if (isset($this->environment['DOCKER_HOST'])) {
$this->logger->info(sprintf('%s Connecting to %s', Emoji::electricPlug(), $this->environment['DOCKER_HOST']));
$this->client = new Guzzle(['base_uri' => $this->environment['DOCKER_HOST']]);
} else {
$this->logger->info(sprintf('%s Connecting to /var/run/docker.sock', Emoji::electricPlug()));
$this->client = new Guzzle(['base_uri' => 'http://localhost', 'curl' => [CURLOPT_UNIX_SOCKET_PATH => '/var/run/docker.sock']]);
}
$this->loader = new FilesystemLoader([
__DIR__,
@ -236,6 +288,9 @@ class Bouncer
// Set up Local certificate store
$this->certificateStoreLocal = new Filesystem(new LocalFilesystemAdapter('/etc/letsencrypt'));
// Set up Local certificate store for certificates provided to us
$this->providedCertificateStore = new Filesystem(new LocalFilesystemAdapter('/certs'));
// Set up Remote certificate store, if configured
if (isset($this->environment['BOUNCER_S3_BUCKET'])) {
$this->certificateStoreRemote = new Filesystem(
@ -255,6 +310,20 @@ class Bouncer
)
);
}
// Allow defined global cert if set
if (isset($this->environment['GLOBAL_CERT'], $this->environment['GLOBAL_CERT_KEY'])) {
$this->setUseGlobalCert(true);
$this->providedCertificateStore->write('global.crt', str_replace('\\n', "\n", trim($this->environment['GLOBAL_CERT'], '"')));
$this->providedCertificateStore->write('global.key', str_replace('\\n', "\n", trim($this->environment['GLOBAL_CERT_KEY'], '"')));
$this->logger->info(sprintf("%s GLOBAL_CERT was set, so we're going to use a defined certificate!", Emoji::globeShowingEuropeAfrica()));
}
// Determine forced update interval.
if (isset($this->environment['BOUNCER_FORCED_UPDATE_INTERVAL_SECONDS']) && is_numeric($this->environment['BOUNCER_FORCED_UPDATE_INTERVAL_SECONDS'])) {
$this->setForcedUpdateIntervalSeconds($this->environment['BOUNCER_FORCED_UPDATE_INTERVAL_SECONDS']);
}
$this->logger->info(sprintf('%s Forced update interval is every %d seconds', Emoji::watch(), $this->getForcedUpdateIntervalSeconds()));
}
public function isSwarmMode(): bool
@ -269,6 +338,30 @@ class Bouncer
return $this;
}
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;
}
/**
* @throws \GuzzleHttp\Exception\GuzzleException
*
@ -288,29 +381,40 @@ class Bouncer
[$envKey, $envVal] = explode('=', $environmentItem, 2);
$envs[$envKey] = $envVal;
} else {
$envs[$envKey] = true;
$envs[$environmentItem] = true;
}
}
}
if (isset($envs['BOUNCER_DOMAIN'])) {
$bouncerTarget = (new BouncerTarget())
$bouncerTarget = (new BouncerTarget($this->logger))
->setId($inspect['Id'])
;
$bouncerTarget = $this->parseContainerEnvironmentVariables($envs, $bouncerTarget);
if (isset($inspect['NetworkSettings']['IPAddress']) && !empty($inspect['NetworkSettings']['IPAddress'])) {
// As per docker service
$bouncerTarget->setIp($inspect['NetworkSettings']['IPAddress']);
$bouncerTarget->setEndpointHostnameOrIp($inspect['NetworkSettings']['IPAddress']);
} else {
// As per docker compose
$networks = array_values($inspect['NetworkSettings']['Networks']);
$bouncerTarget->setIp($networks[0]['IPAddress']);
$bouncerTarget->setEndpointHostnameOrIp($networks[0]['IPAddress']);
}
// $this->logger->debug(sprintf('Decided that %s has the ip %s', $bouncerTarget->getName(), $bouncerTarget->getIp()));
$bouncerTarget->setTargetPath(sprintf('http://%s:%d/', $bouncerTarget->getIp(), $bouncerTarget->getPort()));
$bouncerTarget->setTargetPath(sprintf('http://%s:%d/', $bouncerTarget->getEndpointHostnameOrIp(), $bouncerTarget->getPort() >= 0 ? $bouncerTarget->getPort() : 80));
$bouncerTargets[] = $bouncerTarget;
$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) {
$bouncerTargets[] = $bouncerTarget;
}
}
}
@ -340,19 +444,39 @@ class Bouncer
$envs[$eKey] = $eVal;
}
if (isset($envs['BOUNCER_DOMAIN'])) {
$bouncerTarget = (new BouncerTarget())
$bouncerTarget = (new BouncerTarget($this->logger))
->setId($service['ID'])
;
$bouncerTarget = $this->parseContainerEnvironmentVariables($envs, $bouncerTarget);
$bouncerTarget->setIp('172.17.0.1');
$bouncerTarget->setPort($service['Endpoint']['Ports'][0]['PublishedPort']);
$bouncerTarget->setTargetPath(sprintf('http://%s:%d/', $bouncerTarget->getIp(), $bouncerTarget->getPort()));
if ($bouncerTarget->isPortSet()) {
$bouncerTarget->setEndpointHostnameOrIp($service['Spec']['Name']);
// $this->logger->info(sprintf('Ports for %s has been explicitly set to %s:%d.', $bouncerTarget->getName(), $bouncerTarget->getEndpointHostnameOrIp(), $bouncerTarget->getPort()));
} elseif (isset($service['Endpoint']['Ports'])) {
$bouncerTarget->setEndpointHostnameOrIp('172.17.0.1');
$bouncerTarget->setPort($service['Endpoint']['Ports'][0]['PublishedPort']);
} else {
$this->logger->warning(sprintf('Ports block missing for %s.', $bouncerTarget->getName()));
continue;
}
$bouncerTarget->setTargetPath(sprintf('http://%s:%d/', $bouncerTarget->getEndpointHostnameOrIp(), $bouncerTarget->getPort()));
$bouncerTarget->setUseGlobalCert($this->isUseGlobalCert());
// $this->logger->debug(sprintf('Decided that %s has the target path %s', $bouncerTarget->getName(), $bouncerTarget->getTargetPath()));
$bouncerTargets[] = $bouncerTarget;
$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) {
$bouncerTargets[] = $bouncerTarget;
}
}
}
}
@ -431,6 +555,9 @@ class Bouncer
*/
private function stateHasChanged(): bool
{
if ($this->lastUpdateEpoch === null || $this->lastUpdateEpoch <= time() - $this->forcedUpdateIntervalSeconds) {
return true;
}
$newInstanceStates = [];
// Standard Containers
@ -486,6 +613,8 @@ class Bouncer
$this->logger->info(sprintf('%s Swarm mode is %s.', Emoji::CHARACTER_HONEYBEE, $this->isSwarmMode() ? 'enabled' : 'disabled'));
$targets = $this->isSwarmMode() ? $this->findContainersSwarmMode() : $this->findContainersContainerMode();
$this->wipeNginxConfig();
$this->logger->info(sprintf('%s Found %d services with BOUNCER_DOMAIN set', Emoji::CHARACTER_MAGNIFYING_GLASS_TILTED_LEFT, count($targets)));
foreach ($targets as $target) {
$this->generateNginxConfig($target);
@ -502,6 +631,7 @@ class Bouncer
while ($this->stateHasChanged() === false) {
sleep(5);
}
$this->lastUpdateEpoch = time();
$this->logger->info(sprintf('%s Host Container state has changed', Emoji::CHARACTER_WARNING));
}
@ -567,10 +697,8 @@ class Bouncer
private function generateNginxConfig(BouncerTarget $target): self
{
$this->configFilesystem->write(
$target->getName(),
$this->twig->render('NginxTemplate.twig', $target->__toArray())
);
$configData = $this->twig->render('NginxTemplate.twig', $target->__toArray());
$this->configFilesystem->write($target->getName(), $configData);
$this->logger->info(sprintf('%s Created Nginx config for http://%s', Emoji::CHARACTER_PENCIL, $target->getName()));
return $this;
@ -644,6 +772,19 @@ class Bouncer
$this->logger->info(sprintf('%s Restarting nginx', Emoji::CHARACTER_TIMER_CLOCK));
$shell->run($command);
}
private function wipeNginxConfig(): void
{
$this->logger->debug('Purging existing config files ...');
foreach ($this->configFilesystem->listContents('') as $file) {
/** @var FileAttributes $file */
if ($file->isFile() && $file->path() != 'default' && $file->path() != 'default-ssl') {
$this->configFilesystem->delete($file->path());
// $this->logger->debug(sprintf(' > %s', $file->path()));
}
}
// $this->logger->debug('Purge complete!');
}
}
(new Bouncer())->run();

View file

@ -1,13 +1,13 @@
{
"name": "benzine/bouncer",
"description": "Nginx Configuration Management",
"description": "Automated Docker-swarm aware Nginx configuration management",
"type": "project",
"config": {
"sort-packages": true
},
"license": "GPL-3.0-or-later",
"require": {
"php": "^8.0",
"php": "^8.1",
"ext-json": "*",
"ext-curl": "*",
"kint-php/kint": "^3.3",

138
bouncer/composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "bb6894396a4d90d304b95586f326ea71",
"content-hash": "afde4d97f684020724ec24b67577d3ae",
"packages": [
{
"name": "adambrett/shell-wrapper",
@ -108,16 +108,16 @@
},
{
"name": "aws/aws-sdk-php",
"version": "3.224.0",
"version": "3.228.3",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
"reference": "bc5eb18414ef703c5f39a5a009a437c74c228306"
"reference": "4dad57c95c7ff1dfcea29a7877ce64720b3318c3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/bc5eb18414ef703c5f39a5a009a437c74c228306",
"reference": "bc5eb18414ef703c5f39a5a009a437c74c228306",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/4dad57c95c7ff1dfcea29a7877ce64720b3318c3",
"reference": "4dad57c95c7ff1dfcea29a7877ce64720b3318c3",
"shasum": ""
},
"require": {
@ -125,9 +125,9 @@
"ext-json": "*",
"ext-pcre": "*",
"ext-simplexml": "*",
"guzzlehttp/guzzle": "^5.3.3 || ^6.2.1 || ^7.0",
"guzzlehttp/guzzle": "^6.5.8 || ^7.4.5",
"guzzlehttp/promises": "^1.4.0",
"guzzlehttp/psr7": "^1.7.0 || ^2.1.1",
"guzzlehttp/psr7": "^1.8.5 || ^2.3",
"mtdowling/jmespath.php": "^2.6",
"php": ">=5.5"
},
@ -193,9 +193,9 @@
"support": {
"forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
"issues": "https://github.com/aws/aws-sdk-php/issues",
"source": "https://github.com/aws/aws-sdk-php/tree/3.224.0"
"source": "https://github.com/aws/aws-sdk-php/tree/3.228.3"
},
"time": "2022-05-27T20:23:28+00:00"
"time": "2022-06-24T20:24:09+00:00"
},
{
"name": "bramus/ansi-php",
@ -883,16 +883,16 @@
},
{
"name": "monolog/monolog",
"version": "2.6.0",
"version": "2.7.0",
"source": {
"type": "git",
"url": "https://github.com/Seldaek/monolog.git",
"reference": "247918972acd74356b0a91dfaa5adcaec069b6c0"
"reference": "5579edf28aee1190a798bfa5be8bc16c563bd524"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Seldaek/monolog/zipball/247918972acd74356b0a91dfaa5adcaec069b6c0",
"reference": "247918972acd74356b0a91dfaa5adcaec069b6c0",
"url": "https://api.github.com/repos/Seldaek/monolog/zipball/5579edf28aee1190a798bfa5be8bc16c563bd524",
"reference": "5579edf28aee1190a798bfa5be8bc16c563bd524",
"shasum": ""
},
"require": {
@ -971,7 +971,7 @@
],
"support": {
"issues": "https://github.com/Seldaek/monolog/issues",
"source": "https://github.com/Seldaek/monolog/tree/2.6.0"
"source": "https://github.com/Seldaek/monolog/tree/2.7.0"
},
"funding": [
{
@ -983,7 +983,7 @@
"type": "tidelift"
}
],
"time": "2022-05-10T09:36:00+00:00"
"time": "2022-06-09T08:59:12+00:00"
},
{
"name": "mtdowling/jmespath.php",
@ -1435,16 +1435,16 @@
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.25.0",
"version": "v1.26.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "30885182c981ab175d4d034db0f6f469898070ab"
"reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/30885182c981ab175d4d034db0f6f469898070ab",
"reference": "30885182c981ab175d4d034db0f6f469898070ab",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4",
"reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4",
"shasum": ""
},
"require": {
@ -1459,7 +1459,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
"dev-main": "1.26-dev"
},
"thanks": {
"name": "symfony/polyfill",
@ -1497,7 +1497,7 @@
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.25.0"
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.26.0"
},
"funding": [
{
@ -1513,20 +1513,20 @@
"type": "tidelift"
}
],
"time": "2021-10-20T20:35:02+00:00"
"time": "2022-05-24T11:49:31+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.25.0",
"version": "v1.26.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825"
"reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/0abb51d2f102e00a4eefcf46ba7fec406d245825",
"reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e",
"reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e",
"shasum": ""
},
"require": {
@ -1541,7 +1541,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
"dev-main": "1.26-dev"
},
"thanks": {
"name": "symfony/polyfill",
@ -1580,7 +1580,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.25.0"
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0"
},
"funding": [
{
@ -1596,7 +1596,7 @@
"type": "tidelift"
}
],
"time": "2021-11-30T18:21:41+00:00"
"time": "2022-05-24T11:49:31+00:00"
},
{
"name": "twig/twig",
@ -2337,16 +2337,16 @@
},
{
"name": "symfony/console",
"version": "v6.1.0",
"version": "v6.1.2",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "c9646197ef43b0e2ff44af61e7f0571526fd4170"
"reference": "7a86c1c42fbcb69b59768504c7bca1d3767760b7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/c9646197ef43b0e2ff44af61e7f0571526fd4170",
"reference": "c9646197ef43b0e2ff44af61e7f0571526fd4170",
"url": "https://api.github.com/repos/symfony/console/zipball/7a86c1c42fbcb69b59768504c7bca1d3767760b7",
"reference": "7a86c1c42fbcb69b59768504c7bca1d3767760b7",
"shasum": ""
},
"require": {
@ -2413,7 +2413,7 @@
"terminal"
],
"support": {
"source": "https://github.com/symfony/console/tree/v6.1.0"
"source": "https://github.com/symfony/console/tree/v6.1.2"
},
"funding": [
{
@ -2429,7 +2429,7 @@
"type": "tidelift"
}
],
"time": "2022-05-27T06:34:22+00:00"
"time": "2022-06-26T13:01:30+00:00"
},
{
"name": "symfony/event-dispatcher",
@ -2789,16 +2789,16 @@
},
{
"name": "symfony/polyfill-intl-grapheme",
"version": "v1.25.0",
"version": "v1.26.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-grapheme.git",
"reference": "81b86b50cf841a64252b439e738e97f4a34e2783"
"reference": "433d05519ce6990bf3530fba6957499d327395c2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/81b86b50cf841a64252b439e738e97f4a34e2783",
"reference": "81b86b50cf841a64252b439e738e97f4a34e2783",
"url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/433d05519ce6990bf3530fba6957499d327395c2",
"reference": "433d05519ce6990bf3530fba6957499d327395c2",
"shasum": ""
},
"require": {
@ -2810,7 +2810,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
"dev-main": "1.26-dev"
},
"thanks": {
"name": "symfony/polyfill",
@ -2850,7 +2850,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.25.0"
"source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.26.0"
},
"funding": [
{
@ -2866,20 +2866,20 @@
"type": "tidelift"
}
],
"time": "2021-11-23T21:10:46+00:00"
"time": "2022-05-24T11:49:31+00:00"
},
{
"name": "symfony/polyfill-intl-normalizer",
"version": "v1.25.0",
"version": "v1.26.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-intl-normalizer.git",
"reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8"
"reference": "219aa369ceff116e673852dce47c3a41794c14bd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8590a5f561694770bdcd3f9b5c69dde6945028e8",
"reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8",
"url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/219aa369ceff116e673852dce47c3a41794c14bd",
"reference": "219aa369ceff116e673852dce47c3a41794c14bd",
"shasum": ""
},
"require": {
@ -2891,7 +2891,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
"dev-main": "1.26-dev"
},
"thanks": {
"name": "symfony/polyfill",
@ -2934,7 +2934,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.25.0"
"source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.26.0"
},
"funding": [
{
@ -2950,20 +2950,20 @@
"type": "tidelift"
}
],
"time": "2021-02-19T12:13:01+00:00"
"time": "2022-05-24T11:49:31+00:00"
},
{
"name": "symfony/polyfill-php80",
"version": "v1.25.0",
"version": "v1.26.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
"reference": "4407588e0d3f1f52efb65fbe92babe41f37fe50c"
"reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/4407588e0d3f1f52efb65fbe92babe41f37fe50c",
"reference": "4407588e0d3f1f52efb65fbe92babe41f37fe50c",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/cfa0ae98841b9e461207c13ab093d76b0fa7bace",
"reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace",
"shasum": ""
},
"require": {
@ -2972,7 +2972,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
"dev-main": "1.26-dev"
},
"thanks": {
"name": "symfony/polyfill",
@ -3017,7 +3017,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.25.0"
"source": "https://github.com/symfony/polyfill-php80/tree/v1.26.0"
},
"funding": [
{
@ -3033,20 +3033,20 @@
"type": "tidelift"
}
],
"time": "2022-03-04T08:16:47+00:00"
"time": "2022-05-10T07:21:04+00:00"
},
{
"name": "symfony/polyfill-php81",
"version": "v1.25.0",
"version": "v1.26.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php81.git",
"reference": "5de4ba2d41b15f9bd0e19b2ab9674135813ec98f"
"reference": "13f6d1271c663dc5ae9fb843a8f16521db7687a1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/5de4ba2d41b15f9bd0e19b2ab9674135813ec98f",
"reference": "5de4ba2d41b15f9bd0e19b2ab9674135813ec98f",
"url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/13f6d1271c663dc5ae9fb843a8f16521db7687a1",
"reference": "13f6d1271c663dc5ae9fb843a8f16521db7687a1",
"shasum": ""
},
"require": {
@ -3055,7 +3055,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
"dev-main": "1.26-dev"
},
"thanks": {
"name": "symfony/polyfill",
@ -3096,7 +3096,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php81/tree/v1.25.0"
"source": "https://github.com/symfony/polyfill-php81/tree/v1.26.0"
},
"funding": [
{
@ -3112,7 +3112,7 @@
"type": "tidelift"
}
],
"time": "2021-09-13T13:58:11+00:00"
"time": "2022-05-24T11:49:31+00:00"
},
{
"name": "symfony/process",
@ -3324,16 +3324,16 @@
},
{
"name": "symfony/string",
"version": "v6.1.0",
"version": "v6.1.2",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
"reference": "d3edc75baf9f1d4f94879764dda2e1ac33499529"
"reference": "1903f2879875280c5af944625e8246d81c2f0604"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/string/zipball/d3edc75baf9f1d4f94879764dda2e1ac33499529",
"reference": "d3edc75baf9f1d4f94879764dda2e1ac33499529",
"url": "https://api.github.com/repos/symfony/string/zipball/1903f2879875280c5af944625e8246d81c2f0604",
"reference": "1903f2879875280c5af944625e8246d81c2f0604",
"shasum": ""
},
"require": {
@ -3389,7 +3389,7 @@
"utf8"
],
"support": {
"source": "https://github.com/symfony/string/tree/v6.1.0"
"source": "https://github.com/symfony/string/tree/v6.1.2"
},
"funding": [
{
@ -3405,7 +3405,7 @@
"type": "tidelift"
}
],
"time": "2022-04-22T08:18:23+00:00"
"time": "2022-06-26T16:35:04+00:00"
}
],
"aliases": [],