lyrasoft/firewall

A Firewall package for LYRASOFT

Maintainers

Package info

github.com/lyrasoft/luna-firewall

Type:windwalker-package

pkg:composer/lyrasoft/firewall

Statistics

Installs: 1 196

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 1

0.2.0 2026-03-22 12:46 UTC

This package is auto-updated.

Last update: 2026-03-22 12:47:10 UTC


README

Installation

Install from composer

composer require lyrasoft/firewall

Then copy files to project

php windwalker pkg:install lyrasoft/firewall -t routes -t migrations

Language Files

Add this line to admin & front middleware if you don't want to override languages:

$this->lang->loadAllFromVendor('lyrasoft/firewall', 'ini');

// OR
$this->lang->loadAllFromVendor(\Lyrasoft\Firewall\FirewallPackage::class, 'ini');

Or run this command to copy languages files:

php windwalker pkg:install lyrasoft/firewall -t lang

Register Admin Menu

Edit resources/menu/admin/sidemenu.menu.php

$menu->link($this->trans('unicorn.title.grid', title: $this->trans('firewall.redirect.title')))
    ->to($nav->to('redirect_list')->var('type', 'main'))
    ->icon('fal fa-angles-right');

$menu->link($this->trans('unicorn.title.grid', title: $this->trans('firewall.ip.rule.title')))
    ->to($nav->to('ip_rule_list')->var('type', 'main'))
    ->icon('fal fa-network-wired');

Redirect

Add RedirectMiddleware to etc/app/main.php

use Lyrasoft\Firewall\Middleware\RedirectMiddleware;

    // ...

    'middlewares' => [
        \Windwalker\DI\create(
            RedirectMiddleware::class,
            excludes: [
                'admin/*'
            ]
        ),
        
        // ...
    ],

Now you can add redirect records at admin:

The Source Path Rules

  • Add / at start, the path will compare from site base root (not domain root).
  • If you enable the Regex:
    • Add * will compare a path segment with any string.
    • Add ** will compare cross-segments.
    • You can add custom regex rules, like: /foo/(\d+)

The Dest Path

Thr dest path can be relative path: foo/bar or full URL: https://simular.co/foo/bar.

If you enabne the Regex, you may use variables start with $ to insert matched string. For example, a foo/*/edit/(\d+), can redirect to new/path/$1/edit/$2

Other Params

  • Only 404: Only redirect if a page is 404, if page URL exists, won't redirect.
  • Handle Locale: If this site is multi-language, this params will auto auto detect the starting ;anguage prefix and auto add it to dest path, you may use {lang} in dest path to custom set lang alias position.

Use Different Type from DB

Redirect tables has type colimn, you can use admin/redirect/list/{type} to manage different types.

And if you want to choose types for middleware, you can do this:

    // ...

    'middlewares' => [
        \Windwalker\DI\create(
            RedirectMiddleware::class,
            type: 'other_type',
            excludes: [
                'admin/*'
            ]
        ),
        
        // ...
    ],

The type supports string|Enum|array|null|false, if you send NULL into it, means all redirect records. If you send FALSE, means don't use DB records.

Use Custom List

You can use custom redirect list, custom list will auto-enable the regex:

This settings will merge DB list and custom list.

    // ...

    'middlewares' => [
        \Windwalker\DI\create(
            RedirectMiddleware::class,
            type: 'flower',
            list: [
                'foo/bar' => 'hello/world',            
                'foo/yoo/*' => 'hello/mountain/$1',            
            ],
            excludes: [
                'admin/*'
            ]
        ),
        
        // ...
    ],

This settings will disable DB list and only use custom list.

    // ...

    'middlewares' => [
        \Windwalker\DI\create(
            RedirectMiddleware::class,
            type: false,
            list: [
                'foo/bar' => 'hello/world',            
                'foo/yoo/*' => 'hello/mountain/$1',            
            ],
            excludes: [
                'admin/*'
            ]
        ),
        
        // ...
    ],

Custom List can use Closure to generate list:

// ...

    'middlewares' => [
        \Windwalker\DI\create(
            RedirectMiddleware::class,
            // ...
            list: raw(function (FooService $fooService) {
                return ...; 
            });
        ),
        
        // ...
    ],

The custom list redirect status code default is 301, if you want to use other status, set it to
REDIRECT_DEFAULT_STATUS env varialbe.

Instant Redirect

If there has some reason you can not wait RedirectResponse return, you may use instant redirect:

    // ...

    'middlewares' => [
        \Windwalker\DI\create(
            RedirectMiddleware::class,
            // ...
            instantRedirect: true,
        ),
        
        // ...
    ],

Disable

If you wanr to disable this middleware in debug mode, add this options:

        \Windwalker\DI\create(
            RedirectMiddleware::class,
            enabled: !WINDWALKER_DEBUG
        ),

Hook

Add afterHit hook that you can do somthing or log if redirect hit.

        \Windwalker\DI\create(
            RedirectMiddleware::class,
            afterHit: raw(function (string $dest, \Redirect $redirect) {
                \Windwalker\Core\Manager\Logger::info('Redirect to: ' . $dest);
            })
        ),

RedirectMiddleware params

    public function __construct(
        protected RedirectService $redirectService,
        protected AppContext $app,
        protected bool $enabled = true,
        protected string|\BackedEnum|array|false|null $type = 'main',
        protected array|\Closure|null $list = null,
        protected bool $instantRedirect = false,
        protected array $excludes = [],
        protected ?\Closure $afterHit = null,
        protected int $cacheTtl = 3600,
    ) {
    }
Param Type Default Description
enabled bool true Enable this middleware
type mixed main Entity type, use NULL to select all types, FALSE to disable DB
list array or Closure null Custom redirect list, will merge to DB items if type is not false.
instantRedirect bool false If true, will redirect immediately without waiting RedirectResponse return, this may cause some issues, use with caution.
excludes ?Closure or array null Exclude some paths from redirect, support closure or array, if path match any exclude rule, redirect will not work.
afterHit ?Closure null A hook that will be called after a redirect hit, you can do something or log in this hook.
cacheTtl int 3600 Cache lifetime in seconds, default is 3600 seconds.

IP Allow/Block (Firewall)

To enable IP Rules, add FirewallMiddleware to front.route.php

use Lyrasoft\Firewall\Middleware\FirewallMiddleware;

    // ...

    ->middleware(
        FirewallMiddleware::class,
        // ...
        defaultAction: \Lyrasoft\Firewall\Enum\IpRuleKind::ALLOW, // Or BLOCK, default is ALLOW
        logger: 'firewall/blocks' // Or LoggerInterface instance, default is NULL
    )

    // ...

Admin IP Rules Management

Select Allow or Block, and enter the IP Range format:

The supported formats:

Type Syntax Details
IPV6 ::1 Short notation
IPV4 192.168.0.1
Range 192.168.0.0-192.168.1.60 Includes all IPs from 192.168.0.0 to 192.168.0.255
and from 192.168.1.0 to 198.168.1.60
Wild card 192.168.0.* IPs starting with 192.168.0
Same as IP Range 192.168.0.0-192.168.0.255
Subnet mask 192.168.0.0/255.255.255.0 IPs starting with 192.168.0
Same as 192.168.0.0-192.168.0.255 and 192.168.0.*
CIDR Mask 192.168.0.0/24 IPs starting with 192.168.0
Same as 192.168.0.0-192.168.0.255 and 192.168.0.*
and 192.168.0.0/255.255.255.0

And you can use , to separate multiple IPs or IP Ranges. For example, 0.0.0.0/0,::/0 means all IPs includes both IPV4 and IPV6.

We use mlocati/ip-lib as IP Range parser.

Paths and Domains

After 0.2.0 there has a paths textarea, you can set domains / URLs or paths that this rule will effect, 1 line for 1 path.

Image

For example, if you only want to block some IPs to access admin panel, you can set paths to /admin/*, or /admin/login if you only want to block access to login page.

You can also set domains or full URLs, for example, you can block some IPs to access https://*.foo.com/bar, or https://foo.com/bar/*/baz.

Select DB Type

You can also access different type from ip-rule/list/{type}.

And set type name to middleware

    ->middleware(
        FirewallMiddleware::class,
        type: 'foo',
    )

type can also supports string, array and enum. Use NULL to select all, FALSE to disable DB.

Custom List

If you want to manually set ip list, FirewallMiddleware custom list must use 2 lists, allowList and blockList.

    ->middleware(
        FirewallMiddleware::class,
        type: false,
        allowList: [
            '0.0.0.0',
            '144.122.*.*',
        ],
        blockList: [
            '165.2.90.45',
            '222.44.55.66',
        ],
        allowAsFirst: true, // Or false, default is false
    )

Disable

If you wanr to disable this middleware in debug mode, add this options:

        \Windwalker\DI\create(
            FirewallMiddleware::class,
            enabled: !WINDWALKER_DEBUG
        ),

Hook

Add afterHit hook that you can do somthing or log if an IP was be blocked.

        \Windwalker\DI\create(
            FirewallMiddleware::class,
            afterHit: fn () => function (AppRequest $appRequest) {
                \Windwalker\Core\Manager\Logger::info('Attack from: ' . $appRequest->getClientIp());
            }
        ),

FirewallMiddleware params

Param Type Default Description
enabled bool true Enable this middleware
type mixed main Entity type, use NULL to select all types, FALSE to disable DB
allowList array [] Custom allow list, will merge to DB items.
blockList array [] Custom block list, will merge to DB items.
allowAsFirst bool false When use custom list, if an IP is in both allow and block list, this option will decide which rule is first.
excludes ?Closure or array null Exclude some paths from firewall, support closure or array, if path match any exclude rule, firewall will not work.
defaultAction IpRuleKind IpRuleKind::ALLOW If no IP matched, this option will decide to allow or block this IP.
logger LoggerInterface string or NullLogger Logger instance or logger name, default is NullLogger.
afterHit ?Closure null A hook that will be called after an IP was blocked, you can do something or log in this hook.
cacheTtl int 3600 Cache lifetime in seconds, default is 3600 seconds.
clearExpiredChance number 1/100 Every request has this chance to clear expired cache, default is 1/100, set 0 to disable.

Cache

Cache Lifetime

Both middlewares has a cacheTtl param, default is 3600 seconds.

        \Windwalker\DI\create(
            FirewallMiddleware::class,
            cacheTtl: 3600,
            clearExpiredChance: 1 / 100 // Default is 1/100, means every 100 request will clear expired cache once, set it to 0 to disable auto clear expired cache
        ),

Cache Clear

Everytime you edit Redirect or IpRule will auto clear all caches.

The cache files is located at caches/firewall/, and you can add firewall to clear cache command in composer.json

        "post-autoload-dump": [
        ...
        "php windwalker cache:clear renderer html firewall" <-- Add firewall
],

Cache Disable

Cache will disable in debug mode or when ttl set to 0.

Honeypot

Add Honeypot Middleware to front.route.php, also add FirewallMiddleware below it.

If Honeypot matched, it will auto add am IpRule which blocked current IP. The type must match the FirewallMiddleware type.

use Lyrasoft\Firewall\Middleware\HoneypotMiddleware;
use Lyrasoft\Firewall\Service\Honeypot;

$router->group('front')
    ->namespace('front')
    ->middleware(
        HoneypotMiddleware::class,
        type: 'bot',
        matchParams: fn() => [
            '_ref' => 'preview'
        ],
        // Or use matchCallback to do more complex check:
        matchCallback: fn () => function (#[Input] string $foo) {
            return $foo === 'bar';
        },
        expires: '10minutes', // Default is 1 hour.
    )
    ->middleware(
        FirewallMiddleware::class,
        type: 'bot'
        // ...
    )

Then you can add an invisible link in your frontend page:

<a href="/path/to/any?_ref=preview" style="position: absolute; left: -9999px;">Preview</a>

If a bot click this link, it will be blocked for 10 minutes. You can also set multiple block words:

matchParams: fn() => [
    '_ref' => [
        'preview',
        'internal',
        'test'
    ],
    'from' => [...],
    'f' => [...]
]

Allow Good Bot

It is recommended to use the Honeypot for malicious bots that use random IPs and fake User-Agent strings to mimic real users. Legitimate bots can be identified by their User-Agent and will not click hidden links, so HoneypotMiddleware by default allows requests whose User-Agent contains the keyword bot, preventing accidental blocking of legitimate search engine crawlers and other good bots.

use Lyrasoft\Firewall\Middleware\HoneypotMiddleware;
use Lyrasoft\Firewall\Service\Honeypot;

// ...
    ->middleware(
        HoneypotMiddleware::class,
        // ...
        allowGoodRobot: true, // Default is true.
    )

If you want to detect bot by yourself, you can set allowGoodRobot to false, and use excludes to check if this request is an allowed bot:

use Lyrasoft\Firewall\Middleware\HoneypotMiddleware;
use Lyrasoft\Firewall\Service\Honeypot;

// ...
    ->middleware(
        HoneypotMiddleware::class,
        // ...
        allowGoodRobot: false,
        excludes: fn () => function (\Windwalker\Core\Http\AppRequest $appRequest) {
            $ua = $appRequest->getHeader('user-agent');

            // Allow Googlebot and Amazonbot, you can add more rules here.
            return str_contains($ua, 'googlebot') || str_contains($ua, 'amazonbot');
        }
    )

Built-in Block Words

We provides a set of built-in block words, you can use Honeypot::getBlockWords() to get this list, and use it in matchParams:

use Lyrasoft\Firewall\Middleware\HoneypotMiddleware;
use Lyrasoft\Firewall\Service\Honeypot;

$router->group('front')
    ->namespace('front')
    ->middleware(
        HoneypotMiddleware::class,
        matchParams: fn() => [
            '_ref' => Honeypot::getBlockWords()
        ]
    )

And add auto link to page:

{!! $app->retrieve(\Lyrasoft\Firewall\Service\Honeypot::class)->link() !!}

Honeypot class provides a set of words will randomly add to the link, but only first 3 words will be block words, so you can add some fake words to make it more difficult for bots to detect the honeypot link.

You can set your own block words by:

HONEYPOT_BLOCK_WORDS=preview,internal,test,foo,bar,baz

Honeypot will auto use first 3 words as block words.

HoneypotMiddleware params

public function __construct(
        protected AppContext $app,
        protected FirewallService $firewallService,
        protected BrowserNext $browserNext,
        protected string|\UnitEnum $type = 'bot',
        protected ?array $matchParams = null,
        protected ?\Closure $matchCallback = null,
        protected ?\Closure $blockHandler = null,
        protected bool $allowGoodRobot = true,
        protected \Closure|array|null $excludes = null,
        protected \DateTimeInterface|string $expires = '1hour',
    ) {
    }
Param Type Default Description
type mixed bot Entity type, use NULL to select all types, FALSE to disable DB.
matchParams ?array null Match params, for example, ['_ref' => ['hello', ...]] will match URL params which in this list.
matchCallback ?Closure null A callback that will be called to check if this request should block, the callback should return true if it's a bot, or false or null or void if it's not.
blockHandler ?Closure null A callback that will be called when a bot is detected, you can do something or log in this callback. If not provided, will auto add IP to IpRules
allowGoodRobot bool true Allow any bot which has bot keyword in User-Agent, if a bot trying to simulate a read user, we consider it as bad bot.
excludes ?Closure or array null Exclude some paths from honeypot, support closure or array, if path match any exclude rule, honeypot will not work.
expires DateTimeInterface or string 1hour The expires time for a bot, can be a DateTimeInterface instance or a string that can be parsed by strtotime, for example, 1hour, 30min, 2024-01-01 00:00:00, etc.