Improve release pipeline, add phpstan, general refactoring.

This commit is contained in:
Greyscale 2024-05-18 16:15:55 +02:00
parent 40d816494b
commit 5226b33620
25 changed files with 4419 additions and 447 deletions

View file

@ -1,107 +0,0 @@
name: Build Bouncer
permissions:
contents: read
packages: write
on:
workflow_call:
workflow_dispatch:
push:
branches:
- main
schedule:
- cron: "0 14 * * 2" # 2pm Patch Tuesday
concurrency:
group: ${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
build:
name: Build Docker Swarm Loadbalancer
runs-on: ubuntu-latest
steps:
- name: "Setup: Checkout Source"
uses: actions/checkout@v4
- name: "Setup: Get Date"
id: date
run: |
{
echo "datetime=$(date +'%Y-%m-%d %H:%M:%S')"
echo "date=$(date +'%Y-%m-%d')"
echo "time=$(date +'%H:%M:%S')"
echo "container_build_datetime=$(date -u +'%Y-%m-%dT%H:%M:%S.%3NZ')"
} >> "$GITHUB_OUTPUT"
- name: "Setup: PHP"
uses: shivammathur/setup-php@v2
with:
php-version: 8.2
- name: "Setup: Setup QEMU"
uses: docker/setup-qemu-action@v3
- name: "Setup: Expose GitHub Runtime"
uses: crazy-max/ghaction-github-runtime@v3
- name: "Setup: Setup Docker Buildx"
uses: docker/setup-buildx-action@v3
- name: "Setup: Login to Docker Hub"
uses: docker/login-action@v3
with:
username: matthewbaggett
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: "Setup: Login to GHCR"
uses: docker/login-action@v3
with:
registry: ghcr.io
username: matthewbaggett
password: ${{ secrets.GITHUB_TOKEN }}
- name: "Setup: Find Composer Cache Directory"
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: "Setup: Composer Cache"
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-bouncer-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-bouncer-composer-
- name: "Dependencies: Composer Install"
run: composer install --ignore-platform-reqs
- name: "Build: Build & Push Image"
uses: docker/build-push-action@v5
with:
context: .
target: bouncer
platforms: ${{ !env.ACT && 'linux/amd64,linux/arm64' || 'linux/amd64' }}
pull: true
push: true
build-args: |
GIT_SHA=${{ github.sha }}
GIT_BUILD_ID=${{ github.ref_name }}
BUILD_DATE=${{ steps.date.outputs.container_build_datetime }}
GIT_COMMIT_MESSAGE=${{ github.event.head_commit.message }}
tags: |
benzine/bouncer:latest
ghcr.io/benzine-framework/bouncer:latest
cache-from: ${{ !env.ACT && 'type=gha' || '' }}
cache-to: ${{ !env.ACT && 'type=gha,mode=max' || '' }}
build-contexts: |
php:cli=docker-image://ghcr.io/benzine-framework/php:cli-8.2
- name: "Post-Build: Validate build"
shell: bash
run: |
docker \
run \
--rm \
ghcr.io/benzine-framework/bouncer:latest \
/usr/bin/install-report

36
.github/workflows/build.yml vendored Normal file
View file

@ -0,0 +1,36 @@
name: Build Swarm Loadbalancer
permissions:
contents: read
packages: write
on:
workflow_call:
workflow_dispatch:
push:
branches:
- main
schedule:
- cron: "0 14 * * 2" # 2pm Patch Tuesday
concurrency:
group: ${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
build-container:
uses: ./.github/workflows/docker.build.yml
secrets: inherit
check-php:
uses: ./.github/workflows/php.check.yml
secrets: inherit
check-trunk:
uses: ./.github/workflows/trunk.check.yml
secrets: inherit
release-container:
needs:
- build-container
- check-php
- check-trunk
uses: ./.github/workflows/docker.release.yml
secrets: inherit

72
.github/workflows/docker.build.yml vendored Normal file
View file

@ -0,0 +1,72 @@
name: Build Swarm Loadbalancer
permissions:
contents: read
packages: write
on:
workflow_call:
workflow_dispatch:
concurrency:
group: ${{ github.head_ref || github.run_id }}
cancel-in-progress: true
env:
PLATFORMS: linux/amd64,linux/arm64
CANDIDATE_IMAGE: ghcr.io/benzine-framework/bouncer:build-${{ github.sha }}
jobs:
build:
name: Build Swarm Loadbalancer
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- id: date
run: |
{
echo "datetime=$(date +'%Y-%m-%d %H:%M:%S')"
echo "date=$(date +'%Y-%m-%d')"
echo "time=$(date +'%H:%M:%S')"
echo "container_build_datetime=$(date -u +'%Y-%m-%dT%H:%M:%S.%3NZ')"
} >> "$GITHUB_OUTPUT"
- id: read-php-version
run: echo "php_version=$(jq -r '.require["php"]' composer.json | sed -E 's/[^0-9.]//g')" >> $GITHUB_OUTPUT
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ steps.read-php-version.outputs.php_version }}
- uses: docker/setup-qemu-action@v3
- uses: crazy-max/ghaction-github-runtime@v3
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: matthewbaggett
password: ${{ secrets.GITHUB_TOKEN }}
- id: composer-cache-find
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- id: composer-cache
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: "${{ runner.os }}-bouncer-composer-${{ hashFiles('**/composer.lock') }}"
restore-keys: ${{ runner.os }}-bouncer-composer-
- run: composer install --ignore-platform-reqs
- name: "Build & Push Candidate Image as ${{ env.CANDIDATE_IMAGE }}"
uses: docker/build-push-action@v5
with:
context: .
target: bouncer
build-contexts: |
php:cli=docker-image://ghcr.io/benzine-framework/php:cli-${{ steps.read-php-version.outputs.php_version }}
build-args: |
GIT_SHA=${{ github.sha }}
GIT_BUILD_ID=${{ github.ref_name }}
BUILD_DATE=${{ steps.date.outputs.container_build_datetime }}
GIT_COMMIT_MESSAGE=${{ github.event.head_commit.message }}
platforms: ${{ !env.ACT && env.PLATFORMS || 'linux/amd64' }}
pull: true
push: true
tags: ${{ env.CANDIDATE_IMAGE }}
cache-from: ${{ !env.ACT && 'type=gha' || '' }}
cache-to: ${{ !env.ACT && 'type=gha,mode=max' || '' }}

42
.github/workflows/docker.release.yml vendored Normal file
View file

@ -0,0 +1,42 @@
name: Release Swarm Loadbalancer
permissions:
contents: read
packages: write
on:
workflow_call:
workflow_dispatch:
concurrency:
group: ${{ github.head_ref || github.run_id }}
cancel-in-progress: true
env:
CANDIDATE_IMAGE: ghcr.io/benzine-framework/bouncer:build-${{ github.sha }}
RELEASE_IMAGE_GHCR: ghcr.io/benzine-framework/bouncer:latest
RELEASE_IMAGE_DOCKER: benzine/bouncer:latest
jobs:
release:
name: Release Swarm Loadbalancer
runs-on: ubuntu-latest
strategy:
matrix:
registry:
- ghcr
- docker
fail-fast: false
steps:
- name: "Pull Candidate Image"
run: docker pull ${{ env.CANDIDATE_IMAGE }}
- name: "Login to Docker Hub"
if: matrix.registry == 'docker'
run: docker login -u matthewbaggett -p ${{ secrets.DOCKER_HUB_TOKEN }}
- name: "Login to GHCR"
if: matrix.registry == 'ghcr'
run: docker login ghcr.io -u matthewbaggett -p ${{ secrets.GITHUB_TOKEN }}
- name: "Tag Candidate Image"
run: docker tag ${{ env.CANDIDATE_IMAGE }} ${{ matrix.registry == 'ghcr' && env.RELEASE_IMAGE_GHCR || env.RELEASE_IMAGE_DOCKER }}
- name: "Push Release Image"
run: docker push ${{ matrix.registry == 'ghcr' && env.RELEASE_IMAGE_GHCR || env.RELEASE_IMAGE_DOCKER }}

54
.github/workflows/docker.validate.yml vendored Normal file
View file

@ -0,0 +1,54 @@
name: Validate Swarm Loadbalancer
permissions:
contents: read
packages: write
on:
workflow_call:
workflow_dispatch:
workflow_run:
workflows: ["Build Swarm Loadbalancer"]
types:
- completed
concurrency:
group: ${{ github.head_ref || github.run_id }}
cancel-in-progress: true
env:
CANDIDATE_IMAGE: ghcr.io/benzine-framework/bouncer:build-${{ github.sha }}
jobs:
validate-install-report:
name: Run Install Report
runs-on: ubuntu-latest
steps:
- name: "Post-Build: Validate build"
shell: bash
run: |
docker \
run \
--rm \
${{ env.CANDIDATE_IMAGE }} \
/usr/bin/install-report
validate-dive-report:
name: Run Dive
runs-on: ubuntu-latest
steps:
# Use Dive to inspect the image for junk
- name: "Post-Build: Dive"
uses: wagoodman/dive@v0.10.0
with:
args: ${{ env.CANDIDATE_IMAGE }}
validate-vulnerability-report:
name: Run Trivy
runs-on: ubuntu-latest
steps:
# Inspect the container for security vulnerabilities
- name: "Post-Build: Trivy"
uses: aquasecurity/trivy-action@v0.3.0
with:
image-ref: ${{ env.CANDIDATE_IMAGE }}
format: table
exit-code: 1

69
.github/workflows/php.check.yml vendored Normal file
View file

@ -0,0 +1,69 @@
name: "QC: PHP"
permissions: read-all
on:
workflow_call:
workflow_dispatch:
push:
branches:
- main
schedule:
- cron: "0 11 * * 2" # 11am Patch Tuesday
concurrency:
group: ${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
php-stan:
name: PHPStan
runs-on: ubuntu-latest
permissions:
checks: write # For trunk to post annotations
contents: read # For repo checkout
steps:
- name: "Checkout"
uses: actions/checkout@v4
- name: "Read PHP version from composer.json"
id: read-php-version
run: echo "php_version=$(jq -r '.require["php"]' composer.json | sed -E 's/[^0-9.]//g')" >> $GITHUB_OUTPUT
- name: "Setup PHP"
uses: shivammathur/setup-php@v2
with:
php-version: ${{ steps.read-php-version.outputs.php_version }}
tools: phpstan
- name: Run PHPStan
run: phpstan analyse src
php-cs-fixer:
name: PHP-CS-Fixer
runs-on: ubuntu-latest
permissions:
checks: write # For trunk to post annotations
contents: read # For repo checkout
steps:
- name: "Checkout"
uses: actions/checkout@v4
- name: "Read PHP version from composer.json"
id: read-php-version
run: echo "php_version=$(jq -r '.require["php"]' composer.json | sed -E 's/[^0-9.]//g')" >> $GITHUB_OUTPUT
- name: "Setup PHP"
uses: shivammathur/setup-php@v2
with:
php-version: ${{ steps.read-php-version.outputs.php_version }}
tools: php-cs-fixer
- name: "Run PHP CS Fixer"
run: php-cs-fixer fix --config=.php-cs-fixer.php --diff --verbose
# If there are changed files, create a PR, assign it to whom created the push and fail the build
- name: "Create PR"
uses: peter-evans/create-pull-request@v3
with:
title: "Apply php-cs-fixer changes"
commit-message: "Apply php-cs-fixer changes"
branch: "php-cs-fixer-${{ github.sha }}"
token: ${{ secrets.GITHUB_TOKEN }}
assignees: ${{ github.actor }}
labels: "auto-apply"
body: |
This PR was automatically created to apply php-cs-fixer changes.
Please review the changes and merge if they are correct.

View file

@ -0,0 +1,8 @@
parameters:
level: 5
paths:
- src
- bin
- tests
scanFiles:
- bin/bouncer

View file

@ -1,4 +1,4 @@
# checkov:skip=CKV_DOCKER_3 user cannot be determined at this stage.
# checkov:skip=CKV_DOCKER_3 I don't have time for rootless
FROM php:cli as bouncer
LABEL maintainer="Matthew Baggett <matthew@baggett.me>" \
@ -96,21 +96,8 @@ HEALTHCHECK --start-period=30s \
RUN ls -lah /app /app/bin
# checkov:skip=CKV_DOCKER_3 user cannot be determined at this stage.
FROM php:nginx as test-app-a
COPY test/public-web-a /app/public
# checkov:skip=CKV_DOCKER_3 This is a test container.
FROM php:nginx as test-app
COPY tests/testsites /app/public
HEALTHCHECK --start-period=30s \
CMD curl -s -o /dev/null -w "200" http://localhost:80/ || exit 1
# checkov:skip=CKV_DOCKER_3 user cannot be determined at this stage.
FROM php:nginx as test-app-b
COPY test/public-web-b /app/public
HEALTHCHECK --start-period=30s \
CMD curl -s -o /dev/null -w "200" http://localhost:80/ || exit 1
# checkov:skip=CKV_DOCKER_3 user cannot be determined at this stage.
FROM php:nginx as test-app-c
COPY test/public-web-c /app/public
HEALTHCHECK --start-period=30s \
CMD curl -s -o /dev/null -w "200" http://localhost:80/ || exit 1

View file

@ -1,5 +1,16 @@
# Automatic Swarm Nginx Load Balancer
This is a non-production-ready automatic loadbalancer that works by sniffing the docker socket for changes to the running systems in a docker swarm.
It probably works in k8s too, but I don't use k8s personally.
![GitHub Action: Build Status](https://img.shields.io/github/actions/workflow/status/benzine-framework/docker-swarm-loadbalancer/docker.build.yml?logo=github&label=Build)
![GitHub Action: QC Status](https://img.shields.io/github/actions/workflow/status/benzine-framework/docker-swarm-loadbalancer/trunk.check.yml?logo=github&label=QC)
![Docker Pulls](https://img.shields.io/docker/pulls/benzine/bouncer?logo=docker&label=Docker%20Hub%20Pulls)
![Docker Image Size](https://img.shields.io/docker/image-size/benzine/bouncer?logo=docker&label=Container%20Size)
_\* this doesn't include the number of times it's been pulled from the github container registry, and theres no nice way to pull that from a REST API so there isn't a pretty badge for that._
## Environment variables
This container has its own environment variables, AS WELL AS scanning for some environment variables associated with your services.

View file

@ -1,18 +1,21 @@
{
"name": "benzine/bouncer",
"description": "Automated Docker-swarm aware Nginx configuration management",
"type": "project",
"config": {
"sort-packages": true
},
"license": "GPL-3.0-or-later",
"type": "project",
"authors": [
{
"name": "Matthew Baggett",
"email": "matthew@baggett.me"
}
],
"require": {
"php": "^8.1",
"php": "^8.2",
"ext-curl": "*",
"ext-json": "*",
"ext-openssl": "*",
"adambrett/shell-wrapper": "~1.0",
"bramus/monolog-colored-line-formatter": "~3.1",
"adambrett/shell-wrapper": "^1.0",
"bramus/monolog-colored-line-formatter": "^3.1",
"guzzlehttp/guzzle": "^7.8",
"kint-php/kint": "^3.3",
"league/flysystem": "^2.5",
@ -21,27 +24,40 @@
"nesbot/carbon": "^2.72",
"phpspec/php-diff": "^1.1",
"spatie/emoji": "^2.3",
"symfony/polyfill-php83": "^1.28",
"symfony/yaml": "^6.4",
"twig/twig": "^3.8"
},
"authors": [
{
"name": "Matthew Baggett",
"email": "matthew@baggett.me"
}
],
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.46"
"ergebnis/composer-normalize": "^2.42",
"friendsofphp/php-cs-fixer": "^3.46",
"php-static-analysis/rector-rule": "^0.2.2",
"phpstan/extension-installer": "^1.2.0",
"phpstan/phpstan": "^1.11",
"phpunit/phpunit": "^11",
"rawr/phpunit-data-provider": "^3.3",
"rector/rector": "^1.0",
"rregeer/phpunit-coverage-check": "^0.3.1",
"squizlabs/php_codesniffer": "^3.7"
},
"autoload": {
"psr-4": {
"Bouncer\\": "src/"
}
},
"scripts": {
"fix": "php-cs-fixer fix"
},
"bin": [
"bin/bouncer"
]
],
"config": {
"allow-plugins": {
"ergebnis/composer-normalize": true,
"phpstan/extension-installer": true
},
"sort-packages": true
},
"scripts": {
"fix": "php-cs-fixer fix",
"stan": "phpstan analyse",
"test": "phpunit"
}
}

3852
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -27,12 +27,11 @@ services:
additional_contexts:
- php:nginx=docker-image://ghcr.io/benzine-framework/php:nginx-8.2
volumes:
- ./test/public-web-a:/app/public
- ./tests/testsites:/app/public
environment:
- BOUNCER_DOMAIN=a.web.grey.ooo
- BOUNCER_TARGET_PORT=80
# - BOUNCER_LETSENCRYPT=true
- SITE_NAME=A
web-b:
image: test-app-b
build:
@ -40,8 +39,8 @@ services:
additional_contexts:
- php:nginx=docker-image://ghcr.io/benzine-framework/php:nginx-8.2
volumes:
- ./test/public-web-b:/app/public
- ./tests/testsites:/app/public
environment:
- BOUNCER_DOMAIN=b.web.grey.ooo
- BOUNCER_TARGET_PORT=80
# - BOUNCER_LETSENCRYPT=true
- SITE_NAME=B

1
phpstan.neon.dist Symbolic link
View file

@ -0,0 +1 @@
.trunk/configs/phpstan.neon.dist

62
rector.php Normal file
View file

@ -0,0 +1,62 @@
<?php
declare(strict_types=1);
use PhpStaticAnalysis\RectorRule\AnnotationsToAttributesRector;
use Rector\Config\RectorConfig;
use Rector\Doctrine\Set\DoctrineSetList;
use Rector\Php80\Rector\Class_\AnnotationToAttributeRector;
use Rector\PHPUnit\AnnotationsToAttributes\Rector\Class_\AnnotationWithValueToAttributeRector;
use Rector\PHPUnit\CodeQuality\Rector\Class_\PreferPHPUnitThisCallRector;
use Rector\PHPUnit\Rector\Class_\PreferPHPUnitSelfCallRector;
use Rector\PHPUnit\Set\PHPUnitSetList;
use Rector\Removing\Rector\FuncCall\RemoveFuncCallRector;
use Rector\Renaming\Rector\Name\RenameClassRector;
use Rector\Symfony\Set\SensiolabsSetList;
use Rector\Symfony\Set\SymfonySetList;
use Rector\TypeDeclaration\Rector\ClassMethod\AddVoidReturnTypeWhereNoReturnRector;
$rectorConfig = RectorConfig::configure();
$rectorConfig->withParallel(30);
$rectorConfig->withPreparedSets(
deadCode: true,
codeQuality: true
);
$rectorConfig->withPaths([
__DIR__ . '/bin',
__DIR__ . '/src',
__DIR__ . '/tests',
]);
// uncomment to reach your current PHP version
$rectorConfig->withPhpSets();
$rectorConfig->withSets([
PHPUnitSetList::PHPUNIT_80,
PHPUnitSetList::PHPUNIT_90,
PHPUnitSetList::PHPUNIT_100,
PHPUnitSetList::PHPUNIT_CODE_QUALITY,
PHPUnitSetList::ANNOTATIONS_TO_ATTRIBUTES,
// PhpStaticAnalysisSetList::ANNOTATIONS_TO_ATTRIBUTES,// Implied by PHPUNIT_100
DoctrineSetList::ANNOTATIONS_TO_ATTRIBUTES,
SymfonySetList::ANNOTATIONS_TO_ATTRIBUTES,
SensiolabsSetList::ANNOTATIONS_TO_ATTRIBUTES,
__DIR__ . '/vendor/fakerphp/faker/rector-migrate.php',
]);
$rectorConfig->withConfiguredRule(RemoveFuncCallRector::class, [
'var_dump',
]);
$rectorConfig->withRules([
AddVoidReturnTypeWhereNoReturnRector::class,
AnnotationToAttributeRector::class,
AnnotationWithValueToAttributeRector::class,
AnnotationsToAttributesRector::class,
]);
// Prefer self::assert* over $this->assert* in PHPUnit tests
$rectorConfig->withSkip([
PreferPHPUnitThisCallRector::class,
]);
$rectorConfig->withRules([
PreferPHPUnitSelfCallRector::class,
]);
return $rectorConfig;

View file

@ -7,6 +7,7 @@ namespace Bouncer;
use AdamBrett\ShellWrapper\Command\Builder as CommandBuilder;
use AdamBrett\ShellWrapper\Runners\Exec;
use Aws\S3\S3Client;
use Bouncer\Logger\AbstractLogger;
use GuzzleHttp\Client as Guzzle;
use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\ServerException;
@ -15,7 +16,7 @@ use League\Flysystem\FileAttributes;
use League\Flysystem\Filesystem;
use League\Flysystem\FilesystemException;
use League\Flysystem\Local\LocalFilesystemAdapter;
use Monolog\Logger;
use Bouncer\Logger\Logger;
use Bouncer\Logger\Formatter;
use Spatie\Emoji\Emoji;
use Symfony\Component\Yaml\Yaml;
@ -38,7 +39,7 @@ class Bouncer
private Filesystem $certificateStoreLocal;
private ?Filesystem $certificateStoreRemote = null;
private Filesystem $providedCertificateStore;
private Logger $logger;
private AbstractLogger $logger;
private array $previousContainerState = [];
private array $previousSwarmState = [];
private array $fileHashes;
@ -61,7 +62,7 @@ class Bouncer
$this->settings = new Settings();
$this->logger = new \Bouncer\Logger\Logger(
$this->logger = new Logger(
settings: $this->settings,
processIdProcessor: new Processor\ProcessIdProcessor(),
memoryPeakUsageProcessor: new Processor\MemoryPeakUsageProcessor(),
@ -306,6 +307,7 @@ class Bouncer
$bouncerTarget->setUseGlobalCert($this->isUseGlobalCert());
// @phpstan-ignore-next-line MB: I'm not sure you're right about ->hasCustomNginxConfig only returning false, Stan..
if ($bouncerTarget->isEndpointValid() || $bouncerTarget->hasCustomNginxConfig()) {
$bouncerTargets[] = $bouncerTarget;
} else {
@ -364,6 +366,7 @@ class Bouncer
exit(1);
}
// @phpstan-ignore-next-line Yes, I know this is a loop, that is desired.
while (true) {
$this->runLoop();
}
@ -471,24 +474,6 @@ class Bouncer
return json_decode($this->docker->request('GET', "containers/{$id}/json")->getBody()->getContents(), true);
}
private function dockerEnvHas(string $key, ?array $envs): bool
{
if ($envs === null) {
return false;
}
foreach ($envs as $env) {
if (stripos($env, '=') !== false) {
[$envKey, $envVal] = explode('=', $env, 2);
if ($envKey === $key) {
return true;
}
}
}
return false;
}
private function dockerEnvFilter(?array $envs): array
{
if ($envs === null) {

View file

@ -0,0 +1,417 @@
<?php
declare(strict_types=1);
namespace Bouncer\Logger;
use PhpStaticAnalysis\Attributes\Type;
use PhpStaticAnalysis\Attributes\Param;
use PhpStaticAnalysis\Attributes\Returns;
use Monolog\DateTimeImmutable;
use Monolog\Handler\HandlerInterface;
use Monolog\Level;
use Monolog\LogRecord;
use Monolog\Processor\ProcessorInterface;
use Monolog\ResettableInterface;
use Psr\Log\LoggerInterface;
abstract class AbstractLogger implements LoggerInterface, ResettableInterface
{
protected array $handlers;
protected bool $microsecondTimestamps = true;
protected \DateTimeZone $timezone;
protected ?\Closure $exceptionHandler = null;
/**
* Keeps track of depth to prevent infinite logging loops.
*/
private int $logDepth = 0;
#[Type('\WeakMap<\Fiber<mixed, mixed, mixed, mixed>, int>')] // Keeps track of depth inside fibers to prevent infinite logging loops
private \WeakMap $fiberLogDepth;
/**
* Whether to detect infinite logging loops
* This can be disabled via {@see useLoggingLoopDetection} if you have async handlers that do not play well with this.
*/
private bool $detectCycles = true;
#[Param(name: 'string')] // The logging channel, a simple descriptive name that is attached to all log records
#[Param(handlers: 'HandlerInterface[]')] // optional stack of handlers, the first one in the array is called first, etc
#[Param(processors: 'callable[]')] // Optional array of processors
#[Param(timezone: 'null|\DateTimeZone')] // Optional timezone, if not provided date_default_timezone_get() will be used
#[Param(processors: 'array<(callable(LogRecord):LogRecord | ProcessorInterface)>')]
public function __construct(protected string $name, array $handlers = [], #[Type('array<(callable(LogRecord):LogRecord | ProcessorInterface)>')]
protected array $processors = [], ?\DateTimeZone $timezone = null)
{
$this->setHandlers($handlers);
$this->timezone = $timezone ?? new \DateTimeZone(date_default_timezone_get());
$this->fiberLogDepth = new \WeakMap();
}
public function getName(): string
{
return $this->name;
}
/**
* Return a new cloned instance with the name changed.
*/
#[Returns('static')]
public function withName(string $name): self
{
$new = clone $this;
$new->name = $name;
return $new;
}
public function pushHandler(HandlerInterface $handler): self
{
array_unshift($this->handlers, $handler);
return $this;
}
public function setHandlers(array $handlers): self
{
$this->handlers = [];
foreach (array_reverse($handlers) as $handler) {
$this->pushHandler($handler);
}
return $this;
}
#[Param(callback: 'ProcessorInterface|callable(LogRecord):LogRecord')]
#[Returns('$this')]
public function pushProcessor(callable | ProcessorInterface $callback): self
{
array_unshift($this->processors, $callback);
return $this;
}
#[Returns('$this')]
public function useLoggingLoopDetection(bool $detectCycles): self
{
$this->detectCycles = $detectCycles;
return $this;
}
/**
* Adds a log record.
*/
#[Param(level: 'Level')] // The logging level (a Monolog or RFC 5424 level)
#[Param(message: 'string')] // The log message
#[Param(context: 'mixed[]')] // The log context
#[Param(datetime: 'null|DateTimeImmutable')] // Optional log date to log into the past or future
#[Returns('bool')] // Whether the record has been processed
#[Param(level: 'value-of<Level::VALUES>|Level')]
public function addRecord(Level $level, string $message, array $context = [], ?DateTimeImmutable $datetime = null): bool
{
if ($this->detectCycles) {
if (($fiber = \Fiber::getCurrent()) instanceof \Fiber) {
$logDepth = $this->fiberLogDepth[$fiber] = ($this->fiberLogDepth[$fiber] ?? 0) + 1;
} else {
$logDepth = ++$this->logDepth;
}
} else {
$logDepth = 0;
}
if ($logDepth === 3) {
$this->warning('A possible infinite logging loop was detected and aborted. It appears some of your handler code is triggering logging, see the previous log record for a hint as to what may be the cause.');
return false;
}
if ($logDepth >= 5) { // log depth 4 is let through, so we can log the warning above
return false;
}
$trace = debug_backtrace();
$context = array_merge(
[
'file' => basename($trace[1]['file']),
'line' => $trace[1]['line'],
'pid' => getmypid(),
],
$context
);
try {
$recordInitialized = $this->processors === [];
$record = new LogRecord(
datetime: $datetime ?? new DateTimeImmutable($this->microsecondTimestamps, $this->timezone),
channel: $this->name,
level: $level,
message: $message,
context: $context,
extra: [],
);
$handled = false;
foreach ($this->handlers as $handler) {
if (false === $recordInitialized) {
// skip initializing the record as long as no handler is going to handle it
if (!$handler->isHandling($record)) {
continue;
}
try {
foreach ($this->processors as $processor) {
$record = $processor($record);
}
$recordInitialized = true;
} catch (\Throwable $e) {
$this->handleException($e, $record);
return true;
}
}
// once the record is initialized, send it to all handlers as long as the bubbling chain is not interrupted
try {
$handled = true;
if (true === $handler->handle(clone $record)) {
break;
}
} catch (\Throwable $e) {
$this->handleException($e, $record);
return true;
}
}
return $handled;
} finally {
if ($this->detectCycles) {
if (isset($fiber)) {
--$this->fiberLogDepth[$fiber];
} else {
--$this->logDepth;
}
}
}
}
public function close(): void
{
foreach ($this->handlers as $handler) {
$handler->close();
}
}
public function reset(): void
{
foreach ($this->handlers as $handler) {
if ($handler instanceof ResettableInterface) {
$handler->reset();
}
}
foreach ($this->processors as $processor) {
if ($processor instanceof ResettableInterface) {
$processor->reset();
}
}
}
/**
* Checks whether the Logger has a handler that listens on the given level.
*/
#[Param(level: 'Level')]
public function isHandling(Level $level): bool
{
$record = new LogRecord(
datetime: new DateTimeImmutable($this->microsecondTimestamps, $this->timezone),
channel: $this->name,
message: '',
level: $level,
);
foreach ($this->handlers as $handler) {
if ($handler->isHandling($record)) {
return true;
}
}
return false;
}
/**
* Adds a log record at an arbitrary level.
*
* This method allows for compatibility with common interfaces.
*/
#[Param(level: 'mixed')] // The log level (a Monolog, PSR-3 or RFC 5424 level)
#[Param(message: 'string|\Stringable')] // The log message
#[Param(context: 'mixed[]')] // The log context
#[Param(level: 'Level|LogLevel::*')]
public function log($level, string | \Stringable $message, array $context = []): void
{
if (!$level instanceof Level) {
$level = Level::Critical;
}
$this->addRecord($level, "A Level that wasn't valid was used to write to a Logger: {level}", ['level' => $level]);
$this->addRecord($level, (string) $message, $context);
}
/**
* Adds a log record at the DEBUG level.
*
* This method allows for compatibility with common interfaces.
*/
#[Param(message: 'string|\Stringable')] // The log message
#[Param(context: 'mixed[]')] // The log context
public function debug(string | \Stringable $message, array $context = []): void
{
$this->addRecord(Level::Debug, (string) $message, $context);
}
/**
* Adds a log record at the INFO level.
*
* This method allows for compatibility with common interfaces.
*/
#[Param(message: 'string|\Stringable')] // The log message
#[Param(context: 'mixed[]')] // The log context
public function info(string | \Stringable $message, array $context = []): void
{
$this->addRecord(Level::Info, (string) $message, $context);
}
/**
* Adds a log record at the NOTICE level.
*
* This method allows for compatibility with common interfaces.
*/
#[Param(message: 'string|\Stringable')] // The log message
#[Param(context: 'mixed[]')] // The log context
public function notice(string | \Stringable $message, array $context = []): void
{
$this->addRecord(Level::Notice, (string) $message, $context);
}
/**
* Adds a log record at the WARNING level.
*
* This method allows for compatibility with common interfaces.
*/
#[Param(message: 'string|\Stringable')] // The log message
#[Param(context: 'mixed[]')] // The log context
public function warning(string | \Stringable $message, array $context = []): void
{
$this->addRecord(Level::Warning, (string) $message, $context);
}
/**
* Adds a log record at the ERROR level.
*
* This method allows for compatibility with common interfaces.
*/
#[Param(message: 'string|\Stringable')] // The log message
#[Param(context: 'mixed[]')] // The log context
public function error(string | \Stringable $message, array $context = []): void
{
$this->addRecord(Level::Error, (string) $message, $context);
}
/**
* Adds a log record at the CRITICAL level.
*
* This method allows for compatibility with common interfaces.
*/
#[Param(message: 'string|\Stringable')] // The log message
#[Param(context: 'mixed[]')] // The log context
public function critical(string | \Stringable $message, array $context = []): void
{
$this->addRecord(Level::Critical, (string) $message, $context);
}
/**
* Adds a log record at the ALERT level.
*
* This method allows for compatibility with common interfaces.
*/
#[Param(message: 'string|\Stringable')] // The log message
#[Param(context: 'mixed[]')] // The log context
public function alert(string | \Stringable $message, array $context = []): void
{
$this->addRecord(Level::Alert, (string) $message, $context);
}
/**
* Adds a log record at the EMERGENCY level.
*
* This method allows for compatibility with common interfaces.
*/
#[Param(message: 'string|\Stringable')] // The log message
#[Param(context: 'mixed[]')] // The log context
public function emergency(string | \Stringable $message, array $context = []): void
{
$this->addRecord(Level::Emergency, (string) $message, $context);
}
/**
* Sets the timezone to be used for the timestamp of log records.
*/
#[Returns('$this')]
public function setTimezone(\DateTimeZone $tz): self
{
$this->timezone = $tz;
return $this;
}
/**
* Returns the timezone to be used for the timestamp of log records.
*/
public function getTimezone(): \DateTimeZone
{
return $this->timezone;
}
/**
* Delegates exception management to the custom exception handler,
* or throws the exception if no custom handler is set.
*/
protected function handleException(\Throwable $e, LogRecord $record): void
{
if (!$this->exceptionHandler instanceof \Closure) {
throw $e;
}
($this->exceptionHandler)($e, $record);
}
#[Returns('array<string, mixed>')]
public function __serialize(): array
{
return [
'name' => $this->name,
'handlers' => $this->handlers,
'processors' => $this->processors,
'microsecondTimestamps' => $this->microsecondTimestamps,
'timezone' => $this->timezone,
'exceptionHandler' => $this->exceptionHandler,
'logDepth' => $this->logDepth,
'detectCycles' => $this->detectCycles,
];
}
#[Param(data: 'array<string, mixed>')]
public function __unserialize(array $data): void
{
foreach (['name', 'handlers', 'processors', 'microsecondTimestamps', 'timezone', 'exceptionHandler', 'logDepth', 'detectCycles'] as $property) {
if (isset($data[$property])) {
$this->{$property} = $data[$property];
}
}
$this->fiberLogDepth = new \WeakMap();
}
}

View file

@ -7,7 +7,7 @@ namespace Bouncer\Logger;
use Bouncer\Settings\Settings;
use Monolog\Processor;
class Logger extends \Monolog\Logger
class Logger extends AbstractLogger
{
public function __construct(
private readonly Settings $settings,

View file

@ -4,8 +4,10 @@ declare(strict_types=1);
namespace Bouncer;
use Bouncer\Logger\AbstractLogger;
use Bouncer\Logger\Logger;
use Bouncer\Settings\Settings;
use Psr\Log\LoggerInterface;
use Spatie\Emoji\Emoji;
class Target
@ -27,14 +29,12 @@ class Target
private ?int $proxyTimeoutSeconds = null;
private ?string $username = null;
private ?string $password = null;
private ?string $hostOverride = null;
private ?string $customNginxConfig = null;
private bool $requiresForcedScanning = false;
public function __construct(
private Logger $logger,
private AbstractLogger $logger,
private Settings $settings,
) {
$this->allowNonSSL = $this->settings->get('ssl/allow_non_ssl', true);
@ -94,9 +94,6 @@ class Target
return $this->username;
}
/**
* @param string
*/
public function setUsername(string $username): self
{
$this->username = $username;
@ -298,9 +295,6 @@ class Target
return $this;
}
/**
* @return string
*/
public function getDomains(): array
{
return $this->domains;
@ -420,10 +414,6 @@ class Target
return $this;
}
public function getLogger(): Logger
{
return $this->logger;
}
public function updateLogger(): self
{

View file

@ -1 +0,0 @@
<h1>Website A</h1>

Binary file not shown.

Before

Width: 64px  |  Height: 64px  |  Size: 34 KiB

View file

@ -1 +0,0 @@
<h1>Website B</h1>

Binary file not shown.

Before

Width: 64px  |  Height: 64px  |  Size: 34 KiB

View file

@ -1 +0,0 @@
<h1>Website C</h1>

View file

Before

Width: 64px  |  Height: 64px  |  Size: 34 KiB

After

Width: 64px  |  Height: 64px  |  Size: 34 KiB

View file

@ -0,0 +1,5 @@
<?php
$environment = array_merge($_ENV, $_SERVER);
$site = $environment['SITE_NAME'] ?? 'unknown';
$server = $environment['SERVER_NAME'] ?? gethostname();
printf("<h1>Website %s</h1><p>Running on %s</p>", $site, $server);