lyrasoft/firewall 问题修复 & 功能扩展

解决BUG、新增功能、兼容多环境部署,快速响应你的开发需求

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

lyrasoft/firewall

Composer 安装命令:

composer require lyrasoft/firewall

包简介

A Firewall package for LYRASOFT

README 文档

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.

lyrasoft/firewall 适用场景与选型建议

lyrasoft/firewall 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 1.61k 次下载、GitHub Stars 达 0, 最近一次更新时间为 2024 年 07 月 31 日, 在 PHP 生态内属于活跃度较高的组件。

我们在过去多个企业项目中使用过 lyrasoft/firewall 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 lyrasoft/firewall 我们能提供哪些服务?
定制开发 / 二次开发

基于 lyrasoft/firewall 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 1.61k
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 6
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 1
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2024-07-31