mirror of
https://github.com/rekryt/iplist.git
synced 2025-06-20 16:16:06 +02:00
Initial commit
This commit is contained in:
commit
171e449744
46 changed files with 4638 additions and 0 deletions
2
.docker/php/docker-php-disable-assertions.ini
Normal file
2
.docker/php/docker-php-disable-assertions.ini
Normal file
|
@ -0,0 +1,2 @@
|
|||
[Assertion]
|
||||
zend.assertions = -1
|
9
.docker/php/docker-php-enable-jit.ini
Normal file
9
.docker/php/docker-php-enable-jit.ini
Normal file
|
@ -0,0 +1,9 @@
|
|||
; Extended PHP.ini file to enable JIT.
|
||||
; ====================================
|
||||
; Place this file under /usr/local/etc/php/conf.d/
|
||||
zend_extension=opcache.so
|
||||
opcache.enable_cli=1
|
||||
opcache.jit_buffer_size=500000000
|
||||
opcache.jit=1235
|
||||
;opcache.jit_buffer_size=500000000
|
||||
;opcache.jit=1225
|
15
.docker/php/docker-php.ini
Normal file
15
.docker/php/docker-php.ini
Normal file
|
@ -0,0 +1,15 @@
|
|||
[php]
|
||||
display_errors = On
|
||||
memory_limit = 2048M
|
||||
default_socket_timeout = 120
|
||||
upload_max_filesize = 20M
|
||||
post_max_size = 20M
|
||||
|
||||
[opcache]
|
||||
opcache.revalidate_freq = 0
|
||||
opcache.validate_timestamps = 1
|
||||
opcache.max_accelerated_files = 100000
|
||||
opcache.memory_consumption = 512
|
||||
opcache.interned_strings_buffer = 64
|
||||
opcache.fast_shutdown = 1
|
||||
opcache.error_log = "/var/log/php/opcache.log"
|
6
.dockerignore
Normal file
6
.dockerignore
Normal file
|
@ -0,0 +1,6 @@
|
|||
.git
|
||||
.nuxt
|
||||
.output
|
||||
node_modules
|
||||
tmp
|
||||
package-lock.json
|
43
.editorconfig
Normal file
43
.editorconfig
Normal file
|
@ -0,0 +1,43 @@
|
|||
[*]
|
||||
charset=utf-8
|
||||
end_of_line=lf
|
||||
insert_final_newline=false
|
||||
indent_style=space
|
||||
tab_width=4
|
||||
|
||||
[*.vue]
|
||||
indent_style=space
|
||||
indent_size=4
|
||||
|
||||
[*.scss]
|
||||
indent_style=space
|
||||
indent_size=4
|
||||
|
||||
[*.json]
|
||||
indent_style=space
|
||||
indent_size=4
|
||||
|
||||
[{phpunit.xml.dist,*.jhm,*.xslt,*.xul,*.rng,*.xsl,*.xsd,*.ant,*.tld,*.fxml,*.jrxml,*.xml,*.jnlp,*.wsdl}]
|
||||
indent_style=space
|
||||
indent_size=4
|
||||
|
||||
[*.svg]
|
||||
indent_style=space
|
||||
indent_size=4
|
||||
|
||||
[.editorconfig]
|
||||
indent_style=space
|
||||
indent_size=4
|
||||
|
||||
[{*.gy,*.groovy,*.gant,*.gdsl}]
|
||||
indent_style=space
|
||||
indent_size=4
|
||||
|
||||
[{*.ats,*.ts}]
|
||||
indent_style=space
|
||||
indent_size=4
|
||||
|
||||
[{*.yml,*.yaml}]
|
||||
indent_style=space
|
||||
indent_size=4
|
||||
|
5
.env.example
Normal file
5
.env.example
Normal file
|
@ -0,0 +1,5 @@
|
|||
COMPOSE_PROJECT_NAME="iplist"
|
||||
STORAGE_SAVE_INTERVAL="120"
|
||||
SYS_MEMORY_LIMIT="512M"
|
||||
SYS_TIMEZONE="Europe/Moscow"
|
||||
DEBUG="true"
|
23
.gitattributes
vendored
Normal file
23
.gitattributes
vendored
Normal file
|
@ -0,0 +1,23 @@
|
|||
# Set the default behavior, in case people don't have core.autocrlf set.
|
||||
* text=auto eol=lf
|
||||
|
||||
# Explicitly declare text files you want to always be normalized and converted
|
||||
# to native line endings on checkout.
|
||||
*.c text
|
||||
*.h text
|
||||
|
||||
# Declare files that will always have LF line endings on checkout.
|
||||
*.sln text eol=lf
|
||||
*.php text eol=lf
|
||||
*.js text eol=lf
|
||||
*.ts text eol=lf
|
||||
*.json text eol=lf
|
||||
*.vue text eol=lf
|
||||
*.css text eol=lf
|
||||
*.scss text eol=lf
|
||||
*.sass text eol=lf
|
||||
*.less text eol=lf
|
||||
|
||||
# Denote all files that are truly binary and should not be modified.
|
||||
*.png binary
|
||||
*.jpg binary
|
133
.gitignore
vendored
Normal file
133
.gitignore
vendored
Normal file
|
@ -0,0 +1,133 @@
|
|||
# Dependencies
|
||||
node_modules
|
||||
.output
|
||||
.nitro
|
||||
jspm_packages
|
||||
vendor
|
||||
|
||||
# Only keep yarn.lock in the root
|
||||
package-lock.json
|
||||
*/**/yarn.lock
|
||||
composer.lock
|
||||
|
||||
# Logs
|
||||
/logs
|
||||
*.log*
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Cache
|
||||
.phpunit.result.cache
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Packages
|
||||
packages/*/LICENSE
|
||||
|
||||
# VSCode
|
||||
.vscode
|
||||
|
||||
# Intellij idea
|
||||
*.iml
|
||||
.idea
|
||||
|
||||
# Zend \ Eclipse
|
||||
/.settings/
|
||||
/.buildpath
|
||||
/.project
|
||||
|
||||
# OSX
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
CHANGELOG.md
|
||||
|
||||
TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
# nuxt.js build output
|
||||
.nuxt
|
||||
|
||||
# Nuxt generate
|
||||
dist
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless
|
||||
|
||||
# Vim swap files
|
||||
*.swp
|
||||
|
||||
# Robots Files
|
||||
robots.txt
|
||||
|
||||
# Ignore Service Worker
|
||||
sw.*
|
15
.prettierrc
Normal file
15
.prettierrc
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"useTabs": false,
|
||||
"printWidth": 120,
|
||||
"tabWidth": 4,
|
||||
"trailingComma": "es5",
|
||||
"jsxBracketSameLine": false,
|
||||
"bracketSpacing": true,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"htmlWhitespaceSensitivity": "ignore",
|
||||
"phpVersion": "7.1",
|
||||
"braceStyle": "1tbs",
|
||||
"endOfLine": "lf",
|
||||
"plugins": ["@prettier/plugin-php"]
|
||||
}
|
45
Dockerfile
Normal file
45
Dockerfile
Normal file
|
@ -0,0 +1,45 @@
|
|||
FROM php:8.3-cli
|
||||
|
||||
RUN apt-get update
|
||||
|
||||
# dependencies
|
||||
RUN apt-get install -y ntp whois dnsutils ipcalc
|
||||
|
||||
# composer
|
||||
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/bin --filename=composer \
|
||||
&& chmod 755 /usr/bin/composer
|
||||
|
||||
# pcntl
|
||||
RUN docker-php-ext-configure pcntl --enable-pcntl \
|
||||
&& docker-php-ext-install pcntl
|
||||
|
||||
# pecl/ev
|
||||
RUN pecl install -o -f ev \
|
||||
&& docker-php-ext-enable ev
|
||||
|
||||
# zip
|
||||
RUN apt-get install -y libzip-dev zlib1g-dev zip \
|
||||
&& docker-php-ext-install zip
|
||||
|
||||
RUN rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# php.ini
|
||||
ADD .docker/php/docker-php.ini /usr/local/etc/php/conf.d/docker-php-enable-jit.ini
|
||||
ADD .docker/php/docker-php-disable-assertions.ini /usr/local/etc/php/conf.d/docker-php-disable-assertions.ini
|
||||
ADD .docker/php/docker-php-enable-jit.ini /usr/local/etc/php/conf.d/docker-php-enable-jit.ini
|
||||
|
||||
RUN apt-get clean
|
||||
|
||||
COPY ./src/ /app/src/
|
||||
COPY ./config/ /app/config/
|
||||
COPY ./storage/ /app/storage/
|
||||
COPY ./composer.json /app/
|
||||
COPY ./index.php /app/
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN composer install --no-interaction
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
CMD [ "php", "./index.php" ]
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2024
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
165
README.en.md
Normal file
165
README.en.md
Normal file
|
@ -0,0 +1,165 @@
|
|||
# IP Address Collection and Management Service
|
||||
This service is designed for collecting and updating IP addresses (IPv4 and IPv6) and their CIDR zones for specified domains. It is implemented as a web server using asynchronous PHP 8.1+ with the AMPHP library and integrates with Linux utilities like `whois` and `ipcalc`. The service provides interfaces for retrieving lists of domains, IPv4 addresses, IPv6 addresses, as well as CIDRv4 and CIDRv6 zones in various formats, including plain text, JSON, and scripts for adding to "Address List" on Mikrotik routers (RouterOS).
|
||||

|
||||
|
||||
Demo URL: [https://iplist.opencck.org](https://iplist.opencck.org)
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Domain Monitoring**: Collect and update IP addresses and CIDR zones for specified domains.
|
||||
- **Multi-Format Output**: Supports output in plain text, JSON format, and script format for RouterOS Mikrotik.
|
||||
- **Integration with External Data Sources**: Supports importing initial data from external URLs.
|
||||
- **Easy Deployment with Docker Compose**.
|
||||
- **Configuration through JSON files for managing domains and IPs**.
|
||||
|
||||
## Technologies Used
|
||||
|
||||
- **PHP 8.1+ (amphp, revolt)**
|
||||
- **whois, ipcalc (Linux utilities)**
|
||||
|
||||
## Configuration
|
||||
|
||||
Configuration files are stored in the `config` directory. Each JSON file represents a configuration for a specific portal, defining the domains to monitor and the sources of initial data for IP and CIDR.
|
||||
|
||||
```json
|
||||
{
|
||||
"domains": [
|
||||
"youtube.com",
|
||||
"www.youtube.com",
|
||||
"m.youtube.com",
|
||||
"www.m.youtube.com",
|
||||
"googlevideo.com",
|
||||
"www.googlevideo.com",
|
||||
"ytimg.com",
|
||||
"i.ytimg.com"
|
||||
],
|
||||
"dns": ["127.0.0.11:53", "77.88.8.88:53", "8.8.8.8:53"],
|
||||
"timeout": 43200,
|
||||
"ip4": [],
|
||||
"ip6": [],
|
||||
"cidr4": [],
|
||||
"cidr6": [],
|
||||
"external": {
|
||||
"domains": ["https://raw.githubusercontent.com/nickspaargaren/no-google/master/categories/youtubeparsed"],
|
||||
"ip4": ["https://raw.githubusercontent.com/touhidurrr/iplist-youtube/main/ipv4_list.txt"],
|
||||
"ip6": ["https://raw.githubusercontent.com/touhidurrr/iplist-youtube/main/ipv6_list.txt"],
|
||||
"cidr4": ["https://raw.githubusercontent.com/touhidurrr/iplist-youtube/main/cidr4.txt"],
|
||||
"cidr6": ["https://raw.githubusercontent.com/touhidurrr/iplist-youtube/main/cidr6.txt"]
|
||||
}
|
||||
}
|
||||
```
|
||||
| property | type | description |
|
||||
|----------|----------|--------------------------------------------------------------|
|
||||
| domains | string[] | List of portal domains |
|
||||
| dns | string[] | List of DNS servers for updating IP addresses |
|
||||
| timeout | int | Time interval between domain IP address updates (seconds) |
|
||||
| ip4 | string[] | Initial list of IPv4 addresses |
|
||||
| ip6 | string[] | Initial list of IPv6 addresses |
|
||||
| cidr4 | string[] | Initial list of CIDRv4 zones of IPv4 addresses |
|
||||
| cidr6 | string[] | Initial list of CIDRv6 zones of IPv6 addresses |
|
||||
| external | object | Lists of URLs to retrieve initial data from external sources |
|
||||
|
||||
| property | type | description |
|
||||
|----------|----------|-----------------------------------------------------------------|
|
||||
| domains | string[] | List of URLs to retrieve portal domains |
|
||||
| ip4 | string[] | List of URLs to retrieve initial IPv4 addresses |
|
||||
| ip6 | string[] | List of URLs to retrieve initial IPv6 addresses |
|
||||
| cidr4 | string[] | List of URLs to retrieve initial CIDRv4 zones of IPv4 addresses |
|
||||
| cidr6 | string[] | List of URLs to retrieve initial CIDRv6 zones of IPv6 addresses |
|
||||
|
||||
## Setting Up and Running in Docker
|
||||
```shell
|
||||
git clone git@github.com:rekryt/iplist.git
|
||||
cd iplist
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
If needed, edit the `.env` file:
|
||||
|
||||
| property | default value | description |
|
||||
|-----------------------|---------------|----------------------------------------------------------------|
|
||||
| COMPOSE_PROJECT_NAME | iplist | Name of the compose project |
|
||||
| STORAGE_SAVE_INTERVAL | 120 | Cache save interval for whois (seconds) |
|
||||
| SYS_MEMORY_LIMIT | 512M | Memory limit. 1MB is sufficient for the initial configuration |
|
||||
| SYS_TIMEZONE | Europe/Moscow | List of URLs to obtain initial CIDRv4 zones for IPv4 addresses |
|
||||
| DEBUG | true | Determines the logging level |
|
||||
|
||||
You can access the service in your browser via the HTTP protocol on port 8080:
|
||||
```
|
||||
http://0.0.0.0:8080/
|
||||
http://0.0.0.0:8080/?format=json
|
||||
http://0.0.0.0:8080/?format=json&site=youtube.com&data=domains
|
||||
http://0.0.0.0:8080/?format=text&site=youtube.com&data=ip4
|
||||
http://0.0.0.0:8080/?format=mikrotik&data=cidr4
|
||||
http://0.0.0.0:8080/?format=mikrotik&site=youtube.com&data=cidr4
|
||||
```
|
||||
|
||||
## SSL Setup
|
||||
- Install and configure a reverse proxy, for example, [NginxProxyManager](https://nginxproxymanager.com/guide/#quick-setup).
|
||||
- Create a Docker virtual network:
|
||||
```shell
|
||||
docker network create web
|
||||
```
|
||||
- Configure it in the docker-compose.yml files of both the reverse proxy and this project:
|
||||
```yml
|
||||
services:
|
||||
...
|
||||
app:
|
||||
networks:
|
||||
- web
|
||||
networks:
|
||||
web:
|
||||
external: true
|
||||
name: web
|
||||
```
|
||||
- Remove the ports property from this project's docker-compose.yml file.
|
||||
- Apply the changes:
|
||||
```shell
|
||||
docker compose up -d
|
||||
```
|
||||
- You can view the container name with the command docker compose ps.
|
||||
- In the reverse proxy administration panel, configure the domain to point to `iplist-app-1` on port `8080` and enable SSL.
|
||||
- NginxProxyManager will automatically renew the SSL certificate.
|
||||
|
||||
## Manual Launch (PHP 8.1+)
|
||||
```shell
|
||||
apt-get install -y ntp whois dnsutils ipcalc
|
||||
cp .env.example .env
|
||||
composer install
|
||||
php index.php
|
||||
```
|
||||
|
||||
## Настройка Mikrotik
|
||||
- In the router's admin panel (or via winbox), navigate to System -> Scripts.
|
||||
- Create a new script by clicking "Add new" and give it a name, for example `iplist_youtube_v4_cidr`
|
||||
- In the `Source` field, enter the following code (replace `url` with your server's address, and the protocol in `mode` may differ):
|
||||
```
|
||||
/tool fetch url="https://iplist.opencck.org/?format=mikrotik&site=youtube.com&data=cidr4" mode=https dst-path=iplist_youtube_v4_cidr.rsc
|
||||
:delay 5s
|
||||
:log info "Downloaded iplist_youtube_v4_cidr.rsc succesfully";
|
||||
|
||||
/ip firewall address-list remove [find where comment="youtube.com"];
|
||||
:delay 5s
|
||||
|
||||
/import file-name=iplist_youtube_v4_cidr.rsc
|
||||
:delay 10s
|
||||
:log info "New iplist_youtube_v4_cidr added successfully";
|
||||
```
|
||||

|
||||
- Save the script
|
||||
- Go to System -> Scheduler
|
||||
- Create a new task with a name of your choice, for example `iplist_youtube_v4_cidr`
|
||||
- Set the `Start time` for the task (e.g., `00:05:00`). For Interval, enter `1d 00:00:00`.
|
||||
- In the `On event` field, enter the script name
|
||||
```
|
||||
iplist_youtube_v4_cidr
|
||||
```
|
||||

|
||||
- Open the script in System -> Scripts and run it by clicking the `Run Script` button
|
||||
- In the Logs section, you should see the message `New iplist_youtube_v4_cidr added successfully`
|
||||

|
||||
- In IP -> Firewall -> Address Lists, a new list should appear (in this example, named `youtube.com`)
|
||||

|
||||
|
||||
### License
|
||||
The MIT License (MIT). Please see [LICENSE](./LICENSE) for more information.
|
174
README.md
Normal file
174
README.md
Normal file
|
@ -0,0 +1,174 @@
|
|||
### IP Address Collection and Management Service for Mikrotik RouterOS Script, JSON or Text Format
|
||||
For english readme: [README.en.md](README.en.md)
|
||||
|
||||

|
||||
|
||||
Demo URL: [https://iplist.opencck.org](https://iplist.opencck.org)
|
||||
|
||||
# Сервис сбора IP-адресов и CIDR зон
|
||||
Данный сервис предназначен для сбора и обновления IP-адресов (IPv4 и IPv6), а также их CIDR зон для указанных доменов.
|
||||
Это асинхронный PHP веб-сервер на основе [AMPHP](https://amphp.org/) и с Linux-утилит `whois` и `ipcalc`.
|
||||
Сервис предоставляет интерфейсы для получения списков зон ip адресов указанных доменов (IPv4 адресов, IPv6 адресов, а также CIDRv4 и CIDRv6 зон) в различных форматах, включая текстовый, JSON и формате скрипта для добавления в "Address List" на роутерах Mikrotik (RouterOS).
|
||||
|
||||
Основные возможности
|
||||
- Мониторинг доменов: Сбор и обновление IP-адресов и CIDR зон для указанных доменов.
|
||||
- Многоформатный вывод: Поддержка вывода данных в текстовом формате, формате JSON и в виде скрипта для RouterOS Mikrotik.
|
||||
- Интеграция с внешними источниками данных: поддержка импорта начальных данных из внешних URL.
|
||||
- Легкое развертывание с помощью Docker Compose.
|
||||
- Настройка через JSON файлы для управления доменами и IP.
|
||||
|
||||
Используемые технологии
|
||||
- PHP 8.1+ (amphp, revolt)
|
||||
- whois, ipcalc (linux)
|
||||
|
||||
## Настройки
|
||||
Конфигурационные файлы хранятся в директории `config`. Каждый JSON файл представляет собой конфигурацию для конкретного портала, задавая домены для мониторинга и источники начальных данных по IP и CIDR.
|
||||
```json
|
||||
{
|
||||
"domains": [
|
||||
"youtube.com",
|
||||
"www.youtube.com",
|
||||
"m.youtube.com",
|
||||
"www.m.youtube.com",
|
||||
"googlevideo.com",
|
||||
"www.googlevideo.com",
|
||||
"ytimg.com",
|
||||
"i.ytimg.com"
|
||||
],
|
||||
"dns": ["127.0.0.11:53", "77.88.8.88:53", "8.8.8.8:53"],
|
||||
"timeout": 43200,
|
||||
"ip4": [],
|
||||
"ip6": [],
|
||||
"cidr4": [],
|
||||
"cidr6": [],
|
||||
"external": {
|
||||
"domains": ["https://raw.githubusercontent.com/nickspaargaren/no-google/master/categories/youtubeparsed"],
|
||||
"ip4": ["https://raw.githubusercontent.com/touhidurrr/iplist-youtube/main/ipv4_list.txt"],
|
||||
"ip6": ["https://raw.githubusercontent.com/touhidurrr/iplist-youtube/main/ipv6_list.txt"],
|
||||
"cidr4": ["https://raw.githubusercontent.com/touhidurrr/iplist-youtube/main/cidr4.txt"],
|
||||
"cidr6": ["https://raw.githubusercontent.com/touhidurrr/iplist-youtube/main/cidr6.txt"]
|
||||
}
|
||||
}
|
||||
```
|
||||
| свойство | тип | описание |
|
||||
|----------|----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| domains | string[] | Список доменов портала |
|
||||
| dns | string[] | Список DNS серверов для обновления ip-адресов. По мимо локального и google dns, можно использовать [публичные российские DNS](https://public-dns.info/nameserver/ru.html), например [Яндекс](https://dns.yandex.ru/) |
|
||||
| timeout | int | Время между обновлением ip-адресов доменов (секунды) |
|
||||
| ip4 | string[] | Начальный список ipv4 адресов |
|
||||
| ip6 | string[] | Начальный список ipv6 адресов |
|
||||
| cidr4 | string[] | Начальный список CIDRv4 зон ipv4 адресов |
|
||||
| cidr6 | string[] | Начальный список CIDRv6 зон ipv6 адресов |
|
||||
| external | object | Списки URL для получения начальных данных от сторонних источников |
|
||||
|
||||
| свойство | тип | описание |
|
||||
|----------|----------|------------------------------------------------------------|
|
||||
| domains | string[] | Список URL для получения доменов портала |
|
||||
| ip4 | string[] | Список URL для получения начальных ipv4 адресов |
|
||||
| ip6 | string[] | Список URL для получения начальных ipv6 адресов |
|
||||
| cidr4 | string[] | Список URL для получения начальных CIDRv4 зон ipv4 адресов |
|
||||
| cidr6 | string[] | Список URL для получения начальных CIDRv6 зон ipv6 адресов |
|
||||
|
||||
## Настройка и запуск под docker
|
||||
```shell
|
||||
git clone git@github.com:rekryt/iplist.git
|
||||
cd iplist
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
Если требуется отредактируйте `.env` файл
|
||||
|
||||
| свойство | значение по умолчанию | описание |
|
||||
|-----------------------|-----------------------|---------------------------------------------------------------------|
|
||||
| COMPOSE_PROJECT_NAME | iplist | Имя compose проекта |
|
||||
| STORAGE_SAVE_INTERVAL | 120 | Период сохранения кеша whois (секунды) |
|
||||
| SYS_MEMORY_LIMIT | 512M | Предельное кол-во памяти. Для начальной конфигурации достаточно 1МБ |
|
||||
| SYS_TIMEZONE | Europe/Moscow | Список URL для получения начальных CIDRv4 зон ipv4 адресов |
|
||||
| DEBUG | true | Определяет уровень логирования |
|
||||
|
||||
```shell
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Открыть сервис можно в браузере по протоколу http, порт `8080`
|
||||
```
|
||||
http://0.0.0.0:8080/
|
||||
http://0.0.0.0:8080/?format=json
|
||||
http://0.0.0.0:8080/?format=json&site=youtube.com&data=domains
|
||||
http://0.0.0.0:8080/?format=text&site=youtube.com&data=ip4
|
||||
http://0.0.0.0:8080/?format=mikrotik&data=cidr4
|
||||
http://0.0.0.0:8080/?format=mikrotik&site=youtube.com&data=cidr4
|
||||
```
|
||||
|
||||
## Настройка SSL
|
||||
Для настройки SSL сертификата вам понадобится домен настроенный на ваш сервер.
|
||||
Если у вас нет собственного домена - как вариант бесплатный домен можно получить например на [https://noip.com](https://noip.com).
|
||||
Актуальность такого домена придётся подтверждать раз в месяц.
|
||||
- Установите и настройте реверс-прокси, например [NginxProxyManager](https://nginxproxymanager.com/guide/#quick-setup)
|
||||
- Создайте виртуальную сеть docker
|
||||
```shell
|
||||
docker network create web
|
||||
```
|
||||
- Настройте её в docker-compose.yml файлах реверс-прокси и данного проекта
|
||||
```yml
|
||||
services:
|
||||
...
|
||||
app:
|
||||
networks:
|
||||
- web
|
||||
networks:
|
||||
web:
|
||||
external: true
|
||||
name: web
|
||||
```
|
||||
- Удалите свойство ports из docker-compose.yml (этого проекта)
|
||||
- Примените изменения:
|
||||
```shell
|
||||
docker compose up -d
|
||||
```
|
||||
- Имя контейнера можно посмотреть командой `docker compose ps`
|
||||
- В панели администрирования реверс-прокси настройте домен на него `iplist-app-1` порт `8080` и включите SSL
|
||||
- NginxProxyManager будет продлевать ssl сертификат автоматически
|
||||
|
||||
## Ручной запуск (PHP 8.1+)
|
||||
```shell
|
||||
apt-get install -y ntp whois dnsutils ipcalc
|
||||
cp .env.example .env
|
||||
composer install
|
||||
php index.php
|
||||
```
|
||||
|
||||
## Настройка Mikrotik
|
||||
- В администраторской панели роутера (или через winbox) откройте раздел System -> Scripts
|
||||
- Создайте новый скрипт "Add new" с произвольным именем, например `iplist_youtube_v4_cidr`
|
||||
- В поле `Source` введите следующий код (используйте `url` адрес вашего сервера, протокол в `mode` тоже может отличаться):
|
||||
```
|
||||
/tool fetch url="https://iplist.opencck.org/?format=mikrotik&site=youtube.com&data=cidr4" mode=https dst-path=iplist_youtube_v4_cidr.rsc
|
||||
:delay 5s
|
||||
:log info "Downloaded iplist_youtube_v4_cidr.rsc succesfully";
|
||||
|
||||
/ip firewall address-list remove [find where comment="youtube.com"];
|
||||
:delay 5s
|
||||
|
||||
/import file-name=iplist_youtube_v4_cidr.rsc
|
||||
:delay 10s
|
||||
:log info "New iplist_youtube_v4_cidr added successfully";
|
||||
```
|
||||

|
||||
- Сохраните скрипт
|
||||
- Откройте раздел планировщика System -> Scheduler
|
||||
- Создайте новое задание с произвольным именем, например `iplist_youtube_v4_cidr`
|
||||
- В качестве `Start time` укажите время для старта задания (пример: `00:05:00`). Для `Interval` введите значение `1d 00:00:00`.
|
||||
- В поле `On event` введите имя скрипта
|
||||
```
|
||||
iplist_youtube_v4_cidr
|
||||
```
|
||||

|
||||
- Откройте скрипт в разделе System -> Scripts и запустите его нажатием на кнопку `Run Script`
|
||||
- В разделе Logs вы должны увидеть сообщение `New iplist_youtube_v4_cidr added successfully`
|
||||

|
||||
- А в разделе IP -> Firewall -> Address Lists должен появиться новый список (в примере с именем `youtube.com`)
|
||||

|
||||
|
||||
### License
|
||||
The MIT License (MIT). Please see [LICENSE](./LICENSE) for more information.
|
29
composer.json
Normal file
29
composer.json
Normal file
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"name": "rekryt/iplist",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Rekryt",
|
||||
"homepage": "https://github.com/rekryt"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"opencck/server": "v1.0.0",
|
||||
"amphp/dns": "^2.2",
|
||||
"vlucas/phpdotenv": "^5.5"
|
||||
},
|
||||
"autoload": {
|
||||
"files": [
|
||||
"src/functions.php"
|
||||
],
|
||||
"psr-4": {
|
||||
"OpenCCK\\": [
|
||||
"src/"
|
||||
]
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"php-http/discovery": true
|
||||
}
|
||||
}
|
||||
}
|
25
config/youtube.com.json
Normal file
25
config/youtube.com.json
Normal file
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"domains": [
|
||||
"youtube.com",
|
||||
"www.youtube.com",
|
||||
"m.youtube.com",
|
||||
"www.m.youtube.com",
|
||||
"googlevideo.com",
|
||||
"www.googlevideo.com",
|
||||
"ytimg.com",
|
||||
"i.ytimg.com"
|
||||
],
|
||||
"dns": ["127.0.0.11:53", "77.88.8.88:53", "8.8.8.8:53"],
|
||||
"timeout": 43200,
|
||||
"ip4": [],
|
||||
"ip6": [],
|
||||
"cidr4": [],
|
||||
"cidr6": [],
|
||||
"external": {
|
||||
"domains": ["https://raw.githubusercontent.com/nickspaargaren/no-google/master/categories/youtubeparsed"],
|
||||
"ip4": ["https://raw.githubusercontent.com/touhidurrr/iplist-youtube/main/ipv4_list.txt"],
|
||||
"ip6": ["https://raw.githubusercontent.com/touhidurrr/iplist-youtube/main/ipv6_list.txt"],
|
||||
"cidr4": ["https://raw.githubusercontent.com/touhidurrr/iplist-youtube/main/cidr4.txt"],
|
||||
"cidr6": ["https://raw.githubusercontent.com/touhidurrr/iplist-youtube/main/cidr6.txt"]
|
||||
}
|
||||
}
|
23
docker-compose.yml
Normal file
23
docker-compose.yml
Normal file
|
@ -0,0 +1,23 @@
|
|||
services:
|
||||
app:
|
||||
restart: unless-stopped
|
||||
build:
|
||||
context: .
|
||||
env_file:
|
||||
- ./.env
|
||||
volumes:
|
||||
- ./src/:/app/src/
|
||||
- ./storage/:/app/storage/
|
||||
- ./config/:/app/config/
|
||||
ports:
|
||||
- '8080:8080'
|
||||
logging:
|
||||
driver: 'json-file'
|
||||
options:
|
||||
max-size: '50m'
|
||||
# networks:
|
||||
# - web
|
||||
#networks:
|
||||
# web:
|
||||
# external: true
|
||||
# name: web
|
17
index.php
Normal file
17
index.php
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
use OpenCCK\Infrastructure\API\App;
|
||||
use OpenCCK\Infrastructure\API\Server;
|
||||
|
||||
use Amp\ByteStream\BufferException;
|
||||
|
||||
require_once 'vendor/autoload.php';
|
||||
|
||||
App::getInstance()
|
||||
->addModule(
|
||||
/**
|
||||
* @throws Throwable
|
||||
* @throws BufferException
|
||||
*/ fn(App $app) => Server::getInstance()
|
||||
)
|
||||
->start();
|
11
package.json
Normal file
11
package.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"devDependencies": {
|
||||
"@prettier/plugin-php": "^0.21.0",
|
||||
"prettier": "^3.1.0"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"author": "Rekryt",
|
||||
"license": "MIT"
|
||||
}
|
60
src/App/Controller/AbstractController.php
Normal file
60
src/App/Controller/AbstractController.php
Normal file
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
|
||||
namespace OpenCCK\App\Controller;
|
||||
|
||||
use Amp\Http\HttpStatus;
|
||||
use Amp\Http\Server\Request;
|
||||
use Amp\Http\Server\Response;
|
||||
|
||||
use Throwable;
|
||||
|
||||
abstract class AbstractController implements ControllerInterface {
|
||||
private int $httpStatus = HttpStatus::OK;
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param array $headers
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function __construct(protected Request $request, protected array $headers = []) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Response
|
||||
*/
|
||||
public function __invoke(): Response {
|
||||
return new Response(status: $this->httpStatus, headers: $this->headers, body: $this->getBody());
|
||||
}
|
||||
|
||||
abstract public function getBody(): string;
|
||||
|
||||
public function setHeaders(array $headers): AbstractController {
|
||||
$this->headers = $headers;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $httpStatus
|
||||
*/
|
||||
public function setHttpStatus(int $httpStatus): void {
|
||||
$this->httpStatus = $httpStatus;
|
||||
}
|
||||
|
||||
public function redirect(string $url, bool $permanently = false): void {
|
||||
$this->httpStatus = $permanently ? HttpStatus::MOVED_PERMANENTLY : HttpStatus::SEE_OTHER;
|
||||
$this->headers = array_merge($this->headers ?? [], ['location' => $url]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getBaseURL(): string {
|
||||
$schemePort = ['http' => 80, 'https' => 443];
|
||||
return $this->request->getUri()->getScheme() .
|
||||
'://' .
|
||||
$this->request->getUri()->getHost() .
|
||||
($schemePort[$this->request->getUri()->getScheme()] !== $this->request->getUri()->getPort()
|
||||
? ':' . $this->request->getUri()->getPort()
|
||||
: '');
|
||||
}
|
||||
}
|
35
src/App/Controller/AbstractIPListController.php
Normal file
35
src/App/Controller/AbstractIPListController.php
Normal file
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
namespace OpenCCK\App\Controller;
|
||||
|
||||
use Amp\ByteStream\BufferException;
|
||||
use Amp\Http\Server\Request;
|
||||
|
||||
use OpenCCK\App\Service\IPListService;
|
||||
use OpenCCK\Infrastructure\API\App;
|
||||
|
||||
use Monolog\Logger;
|
||||
use Throwable;
|
||||
|
||||
abstract class AbstractIPListController extends AbstractController {
|
||||
protected Logger $logger;
|
||||
protected IPListService $service;
|
||||
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param array $headers
|
||||
* @throws BufferException
|
||||
* @throws Throwable
|
||||
*/
|
||||
public function __construct(protected Request $request, protected array $headers = []) {
|
||||
parent::__construct($request, $this->headers);
|
||||
|
||||
$this->logger = App::getLogger();
|
||||
$this->service = IPListService::getInstance();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
abstract public function getBody(): string;
|
||||
}
|
19
src/App/Controller/ControllerInterface.php
Normal file
19
src/App/Controller/ControllerInterface.php
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
namespace OpenCCK\App\Controller;
|
||||
|
||||
use Amp\Http\Server\Request;
|
||||
use Amp\Http\Server\Response;
|
||||
|
||||
interface ControllerInterface {
|
||||
/**
|
||||
* @param Request $request
|
||||
* @param array $headers
|
||||
*/
|
||||
public function __construct(Request $request, array $headers = []);
|
||||
|
||||
/**
|
||||
* @return Response
|
||||
*/
|
||||
public function __invoke(): Response;
|
||||
}
|
32
src/App/Controller/JsonController.php
Normal file
32
src/App/Controller/JsonController.php
Normal file
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
namespace OpenCCK\App\Controller;
|
||||
|
||||
class JsonController extends AbstractIPListController {
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getBody(): string {
|
||||
$this->setHeaders(['content-type' => 'application/json']);
|
||||
|
||||
$site = $this->request->getQueryParameter('site') ?? '';
|
||||
$data = $this->request->getQueryParameter('data') ?? '';
|
||||
if ($site == '') {
|
||||
if ($data == '') {
|
||||
return json_encode($this->service->sites);
|
||||
} else {
|
||||
$result = [];
|
||||
foreach ($this->service->sites as $site) {
|
||||
$result[$site->name] = $site->$data;
|
||||
}
|
||||
return json_encode($result);
|
||||
}
|
||||
} else {
|
||||
if ($data == '') {
|
||||
return json_encode($this->service->sites[$site]);
|
||||
} else {
|
||||
return json_encode($this->service->sites[$site]->$data);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
23
src/App/Controller/MainController.php
Normal file
23
src/App/Controller/MainController.php
Normal file
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
namespace OpenCCK\App\Controller;
|
||||
|
||||
class MainController extends AbstractIPListController {
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getBody(): string {
|
||||
$this->setHeaders(['content-type' => 'text/html; charset=utf-8']);
|
||||
return $this->renderTemplate('index');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $template
|
||||
* @return string
|
||||
*/
|
||||
private function renderTemplate(string $template): string {
|
||||
ob_start();
|
||||
include PATH_ROOT . '/src/App/Template/' . ucfirst($template) . 'Template.php';
|
||||
return ob_get_clean();
|
||||
}
|
||||
}
|
41
src/App/Controller/MikrotikController.php
Normal file
41
src/App/Controller/MikrotikController.php
Normal file
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
namespace OpenCCK\App\Controller;
|
||||
|
||||
class MikrotikController extends AbstractIPListController {
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getBody(): string {
|
||||
$this->setHeaders(['content-type' => 'text/plain']);
|
||||
|
||||
$site = $this->request->getQueryParameter('site') ?? '';
|
||||
$data = $this->request->getQueryParameter('data') ?? '';
|
||||
if ($data == '') {
|
||||
return "# Error: The 'data' GET parameter is required in the URL to access this page, but it cannot have the value 'All'";
|
||||
}
|
||||
$response = '/ip firewall address-list' . "\n";
|
||||
if ($site == '') {
|
||||
foreach ($this->service->sites as $site) {
|
||||
$response .= $this->render($site->name, $site->$data);
|
||||
}
|
||||
return $response;
|
||||
} else {
|
||||
return $response . $this->render($site, $this->service->sites[$site]->$data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $site
|
||||
* @param iterable $array
|
||||
* @return string
|
||||
*/
|
||||
private function render(string $site, iterable $array): string {
|
||||
$response = '';
|
||||
$listName = str_replace(' ', '', $site);
|
||||
foreach ($array as $item) {
|
||||
$response .= 'add list=' . $listName . ' address=' . $item . ' comment=' . $listName . "\n";
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
}
|
36
src/App/Controller/TextController.php
Normal file
36
src/App/Controller/TextController.php
Normal file
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
namespace OpenCCK\App\Controller;
|
||||
|
||||
class TextController extends AbstractIPListController {
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getBody(): string {
|
||||
$this->setHeaders(['content-type' => 'text/plain']);
|
||||
|
||||
$site = $this->request->getQueryParameter('site') ?? '';
|
||||
$data = $this->request->getQueryParameter('data') ?? '';
|
||||
if ($data == '') {
|
||||
return "# Error: The 'data' GET parameter is required in the URL to access this page, but it cannot have the value 'All'";
|
||||
}
|
||||
|
||||
$response = '';
|
||||
if ($site == '') {
|
||||
foreach ($this->service->sites as $site) {
|
||||
$response .= $this->render($site->name, $site->$data);
|
||||
}
|
||||
return $response;
|
||||
} else {
|
||||
return $this->render($site, $this->service->sites[$site]->$data);
|
||||
}
|
||||
}
|
||||
|
||||
private function render(string $name, iterable $array): string {
|
||||
$response = '# ' . $name . ' ' . date('Y-m-d H:i:s') . "\n";
|
||||
foreach ($array as $item) {
|
||||
$response .= $item . "\n";
|
||||
}
|
||||
return $response;
|
||||
}
|
||||
}
|
50
src/App/Service/IPListService.php
Normal file
50
src/App/Service/IPListService.php
Normal file
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
namespace OpenCCK\App\Service;
|
||||
|
||||
use OpenCCK\Domain\Entity\Site;
|
||||
use OpenCCK\Domain\Factory\SiteFactory;
|
||||
use OpenCCK\Infrastructure\API\App;
|
||||
|
||||
use Exception;
|
||||
use Monolog\Logger;
|
||||
|
||||
class IPListService {
|
||||
private static IPListService $_instance;
|
||||
private ?Logger $logger;
|
||||
|
||||
/**
|
||||
* @var array<string, Site>
|
||||
*/
|
||||
public array $sites = [];
|
||||
|
||||
/**
|
||||
* @throws Exception
|
||||
*/
|
||||
private function __construct(Logger $logger = null) {
|
||||
$this->logger = $logger ?? App::getLogger();
|
||||
|
||||
$dir = PATH_ROOT . '/config/';
|
||||
if (!is_dir($dir)) {
|
||||
throw new Exception('config directory not found');
|
||||
}
|
||||
foreach (scandir($dir) as $file) {
|
||||
if (str_ends_with($file, '.json')) {
|
||||
$this->loadConfig(substr($file, 0, -5), json_decode(file_get_contents($dir . $file)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?Logger $logger
|
||||
* @return IPListService
|
||||
* @throws Exception
|
||||
*/
|
||||
public static function getInstance(Logger $logger = null): IPListService {
|
||||
return self::$_instance ??= new self($logger);
|
||||
}
|
||||
|
||||
private function loadConfig(string $name, object $config): void {
|
||||
$this->sites[$name] = SiteFactory::create($name, $config);
|
||||
}
|
||||
}
|
588
src/App/Template/IndexTemplate.php
Normal file
588
src/App/Template/IndexTemplate.php
Normal file
|
@ -0,0 +1,588 @@
|
|||
<?php
|
||||
use OpenCCK\App\Controller\TextController;
|
||||
/** @var TextController $this */
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>IPList</title>
|
||||
<style>
|
||||
/* MVP.css v1.15 - https://github.com/andybrewer/mvp */
|
||||
:root {
|
||||
--active-brightness: 0.85;
|
||||
--border-radius: 5px;
|
||||
--box-shadow: 2px 2px 10px;
|
||||
--color-accent: #118bee15;
|
||||
--color-bg: #fff;
|
||||
--color-bg-secondary: #e9e9e9;
|
||||
--color-link: #118bee;
|
||||
--color-secondary: #920de9;
|
||||
--color-secondary-accent: #920de90b;
|
||||
--color-shadow: #f4f4f4;
|
||||
--color-table: #118bee;
|
||||
--color-text: #000;
|
||||
--color-text-secondary: #999;
|
||||
--color-scrollbar: #cacae8;
|
||||
--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
|
||||
--hover-brightness: 1.2;
|
||||
--justify-important: center;
|
||||
--justify-normal: left;
|
||||
--line-height: 1.5;
|
||||
--width-card: 285px;
|
||||
--width-card-medium: 460px;
|
||||
--width-card-wide: 800px;
|
||||
--width-content: 1080px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root[color-mode="user"] {
|
||||
--color-accent: #0097fc4f;
|
||||
--color-bg: #333;
|
||||
--color-bg-secondary: #555;
|
||||
--color-link: #0097fc;
|
||||
--color-secondary: #e20de9;
|
||||
--color-secondary-accent: #e20de94f;
|
||||
--color-shadow: #bbbbbb20;
|
||||
--color-table: #0097fc;
|
||||
--color-text: #f7f7f7;
|
||||
--color-text-secondary: #aaa;
|
||||
}
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html {
|
||||
scroll-behavior: auto;
|
||||
}
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
article aside {
|
||||
background: var(--color-secondary-accent);
|
||||
border-left: 4px solid var(--color-secondary);
|
||||
padding: 0.01rem 0.8rem;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--color-bg);
|
||||
color: var(--color-text);
|
||||
font-family: var(--font-family);
|
||||
line-height: var(--line-height);
|
||||
margin: 0;
|
||||
overflow-x: hidden;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
footer,
|
||||
header,
|
||||
main {
|
||||
margin: 0 auto;
|
||||
max-width: var(--width-content);
|
||||
padding: 3rem 1rem;
|
||||
}
|
||||
|
||||
hr {
|
||||
background-color: var(--color-bg-secondary);
|
||||
border: none;
|
||||
height: 1px;
|
||||
margin: 4rem 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
section {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: var(--justify-important);
|
||||
}
|
||||
|
||||
section img,
|
||||
article img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
section pre {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
section aside {
|
||||
border: 1px solid var(--color-bg-secondary);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--box-shadow) var(--color-shadow);
|
||||
margin: 1rem;
|
||||
padding: 1.25rem;
|
||||
width: var(--width-card);
|
||||
}
|
||||
|
||||
section aside:hover {
|
||||
box-shadow: var(--box-shadow) var(--color-bg-secondary);
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Headers */
|
||||
article header,
|
||||
div header,
|
||||
main header {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
header {
|
||||
text-align: var(--justify-important);
|
||||
}
|
||||
|
||||
header a b,
|
||||
header a em,
|
||||
header a i,
|
||||
header a strong {
|
||||
margin-left: 0.5rem;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
header nav img {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
section header {
|
||||
padding-top: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Nav */
|
||||
nav {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
font-weight: bold;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 7rem;
|
||||
}
|
||||
|
||||
nav ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
nav ul li {
|
||||
display: inline-block;
|
||||
margin: 0 0.5rem;
|
||||
position: relative;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* Nav Dropdown */
|
||||
nav ul li:hover ul {
|
||||
display: block;
|
||||
}
|
||||
|
||||
nav ul li ul {
|
||||
background: var(--color-bg);
|
||||
border: 1px solid var(--color-bg-secondary);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--box-shadow) var(--color-shadow);
|
||||
display: none;
|
||||
height: auto;
|
||||
left: -2px;
|
||||
padding: .5rem 1rem;
|
||||
position: absolute;
|
||||
top: 1.7rem;
|
||||
white-space: nowrap;
|
||||
width: auto;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
nav ul li ul::before {
|
||||
/* fill gap above to make mousing over them easier */
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: -0.5rem;
|
||||
height: 0.5rem;
|
||||
}
|
||||
|
||||
nav ul li ul li,
|
||||
nav ul li ul li a {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
code,
|
||||
samp {
|
||||
background-color: var(--color-accent);
|
||||
border-radius: var(--border-radius);
|
||||
color: var(--color-text);
|
||||
display: inline-block;
|
||||
margin: 0 0.1rem;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
|
||||
details {
|
||||
margin: 1.3rem 0;
|
||||
}
|
||||
|
||||
details summary {
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
line-height: var(--line-height);
|
||||
}
|
||||
|
||||
mark {
|
||||
padding: 0.1rem;
|
||||
}
|
||||
|
||||
ol li,
|
||||
ul li {
|
||||
padding: 0.2rem 0;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0.75rem 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 1rem 0;
|
||||
max-width: var(--width-card-wide);
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
pre code,
|
||||
pre samp {
|
||||
display: block;
|
||||
max-width: var(--width-card-wide);
|
||||
padding: 0.5rem 2rem;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
small {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
sup {
|
||||
background-color: var(--color-secondary);
|
||||
border-radius: var(--border-radius);
|
||||
color: var(--color-bg);
|
||||
font-size: xx-small;
|
||||
font-weight: bold;
|
||||
margin: 0.2rem;
|
||||
padding: 0.2rem 0.3rem;
|
||||
position: relative;
|
||||
top: -2px;
|
||||
}
|
||||
|
||||
/* Links */
|
||||
a {
|
||||
color: var(--color-link);
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
filter: brightness(var(--hover-brightness));
|
||||
}
|
||||
|
||||
a:active {
|
||||
filter: brightness(var(--active-brightness));
|
||||
}
|
||||
|
||||
a b,
|
||||
a em,
|
||||
a i,
|
||||
a strong,
|
||||
button,
|
||||
input[type="submit"] {
|
||||
border-radius: var(--border-radius);
|
||||
display: inline-block;
|
||||
font-size: medium;
|
||||
font-weight: bold;
|
||||
line-height: var(--line-height);
|
||||
margin: 0.5rem 0;
|
||||
padding: 1rem 2rem;
|
||||
}
|
||||
|
||||
button,
|
||||
input[type="submit"] {
|
||||
font-family: var(--font-family);
|
||||
}
|
||||
|
||||
button:hover,
|
||||
input[type="submit"]:hover {
|
||||
cursor: pointer;
|
||||
filter: brightness(var(--hover-brightness));
|
||||
}
|
||||
|
||||
button:active,
|
||||
input[type="submit"]:active {
|
||||
filter: brightness(var(--active-brightness));
|
||||
}
|
||||
|
||||
a b,
|
||||
a strong,
|
||||
button,
|
||||
input[type="submit"] {
|
||||
background-color: var(--color-link);
|
||||
border: 2px solid var(--color-link);
|
||||
color: var(--color-bg);
|
||||
}
|
||||
|
||||
a em,
|
||||
a i {
|
||||
border: 2px solid var(--color-link);
|
||||
border-radius: var(--border-radius);
|
||||
color: var(--color-link);
|
||||
display: inline-block;
|
||||
padding: 1rem 2rem;
|
||||
}
|
||||
|
||||
article aside a {
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
|
||||
/* Images */
|
||||
figure {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
figure img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
figure figcaption {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
button:disabled,
|
||||
input:disabled {
|
||||
background: var(--color-bg-secondary);
|
||||
border-color: var(--color-bg-secondary);
|
||||
color: var(--color-text-secondary);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
button[disabled]:hover,
|
||||
input[type="submit"][disabled]:hover {
|
||||
filter: none;
|
||||
}
|
||||
|
||||
form {
|
||||
border: 1px solid var(--color-bg-secondary);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--box-shadow) var(--color-shadow);
|
||||
display: block;
|
||||
max-width: var(--width-card-wide);
|
||||
min-width: var(--width-card);
|
||||
padding: 1.5rem;
|
||||
text-align: var(--justify-normal);
|
||||
}
|
||||
|
||||
form header {
|
||||
margin: 1.5rem 0;
|
||||
padding: 1.5rem 0;
|
||||
}
|
||||
|
||||
input,
|
||||
label,
|
||||
select,
|
||||
textarea {
|
||||
display: block;
|
||||
font-size: inherit;
|
||||
max-width: var(--width-card-wide);
|
||||
}
|
||||
|
||||
input[type="checkbox"],
|
||||
input[type="radio"] {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
input[type="checkbox"]+label,
|
||||
input[type="radio"]+label {
|
||||
display: inline-block;
|
||||
font-weight: normal;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
input[type="range"] {
|
||||
padding: 0.4rem 0;
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
border: 1px solid var(--color-bg-secondary);
|
||||
border-radius: var(--border-radius);
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.4rem 0.8rem;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="password"],
|
||||
textarea {
|
||||
width: calc(100% - 1.6rem);
|
||||
}
|
||||
|
||||
input[readonly],
|
||||
textarea[readonly] {
|
||||
background-color: var(--color-bg-secondary);
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
/* Popups */
|
||||
dialog {
|
||||
border: 1px solid var(--color-bg-secondary);
|
||||
border-radius: var(--border-radius);
|
||||
box-shadow: var(--box-shadow) var(--color-shadow);
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 50%;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
table {
|
||||
border: 1px solid var(--color-bg-secondary);
|
||||
border-radius: var(--border-radius);
|
||||
border-spacing: 0;
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
padding: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
table td,
|
||||
table th,
|
||||
table tr {
|
||||
padding: 0.4rem 0.8rem;
|
||||
text-align: var(--justify-important);
|
||||
}
|
||||
|
||||
table thead {
|
||||
background-color: var(--color-table);
|
||||
border-collapse: collapse;
|
||||
border-radius: var(--border-radius);
|
||||
color: var(--color-bg);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
table thead tr:first-child th:first-child {
|
||||
border-top-left-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
table thead tr:first-child th:last-child {
|
||||
border-top-right-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
table thead th:first-child,
|
||||
table tr td:first-child {
|
||||
text-align: var(--justify-normal);
|
||||
}
|
||||
|
||||
table tr:nth-child(even) {
|
||||
background-color: var(--color-accent);
|
||||
}
|
||||
|
||||
/* Quotes */
|
||||
blockquote {
|
||||
display: block;
|
||||
font-size: x-large;
|
||||
line-height: var(--line-height);
|
||||
margin: 1rem auto;
|
||||
max-width: var(--width-card-medium);
|
||||
padding: 1.5rem 1rem;
|
||||
text-align: var(--justify-important);
|
||||
}
|
||||
|
||||
blockquote footer {
|
||||
color: var(--color-text-secondary);
|
||||
display: block;
|
||||
font-size: small;
|
||||
line-height: var(--line-height);
|
||||
padding: 1.5rem 0;
|
||||
}
|
||||
|
||||
/* Scrollbars */
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--color-scrollbar) transparent;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
*::-webkit-scrollbar-thumb {
|
||||
background-color: var(--color-scrollbar);
|
||||
border-radius: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<form action="" method="get">
|
||||
<section>
|
||||
<label>
|
||||
Format:
|
||||
<select name="format">
|
||||
<option value="json">JSON</option>
|
||||
<option value="text">Text</option>
|
||||
<option value="mikrotik">MikroTik</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Site:
|
||||
<select name="site">
|
||||
<option value="">All</option>
|
||||
<?php foreach ($this->service->sites as $site): ?>
|
||||
<option value="<?= $site->name ?>"><?= $site->name ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
Data:
|
||||
<select name="data">
|
||||
<option value="">All</option>
|
||||
<option value="domains">domains</option>
|
||||
<option value="ip4">ip4</option>
|
||||
<option value="cidr4">cidr4</option>
|
||||
<option value="ip6">ip6</option>
|
||||
<option value="cidr6">cidr6</option>
|
||||
</select>
|
||||
</label>
|
||||
</section>
|
||||
<section>
|
||||
<button type="submit">Submit</button>
|
||||
</section>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
115
src/Domain/Entity/Site.php
Normal file
115
src/Domain/Entity/Site.php
Normal file
|
@ -0,0 +1,115 @@
|
|||
<?php
|
||||
|
||||
namespace OpenCCK\Domain\Entity;
|
||||
|
||||
use OpenCCK\Domain\Factory\SiteFactory;
|
||||
use OpenCCK\Domain\Helper\DNSHelper;
|
||||
use OpenCCK\Domain\Helper\IP4Helper;
|
||||
use OpenCCK\Domain\Helper\IP6Helper;
|
||||
use OpenCCK\Infrastructure\API\App;
|
||||
|
||||
use Revolt\EventLoop;
|
||||
use stdClass;
|
||||
|
||||
final class Site {
|
||||
private DNSHelper $dnsHelper;
|
||||
|
||||
/**
|
||||
* @param string $name Name of portal
|
||||
* @param array $domains List of portal domains
|
||||
* @param array $dns List of DNS servers for updating IP addresses
|
||||
* @param int $timeout Time interval between domain IP address updates (seconds)
|
||||
* @param array $ip4 List of IPv4 addresses
|
||||
* @param array $ip6 List of IPv6 addresses
|
||||
* @param array $cidr4 List of CIDRv4 zones of IPv4 addresses
|
||||
* @param array $cidr6 List of CIDRv6 zones of IPv6 addresses
|
||||
* @param object $external Lists of URLs to retrieve data from external sources
|
||||
*
|
||||
*/
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public array $domains = [],
|
||||
public array $dns = [],
|
||||
public int $timeout = 1440 * 60,
|
||||
public array $ip4 = [],
|
||||
public array $ip6 = [],
|
||||
public array $cidr4 = [],
|
||||
public array $cidr6 = [],
|
||||
public object $external = new stdClass()
|
||||
) {
|
||||
$this->dnsHelper = new DNSHelper($dns);
|
||||
EventLoop::delay(0, $this->reload(...));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
private function reload(): void {
|
||||
$ip4 = [];
|
||||
$ip6 = [];
|
||||
foreach ($this->domains as $domain) {
|
||||
[$ipv4results, $ipv6results] = $this->dnsHelper->resolve($domain);
|
||||
$ip4 = array_merge($ip4, $ipv4results);
|
||||
$ip6 = array_merge($ip6, $ipv6results);
|
||||
}
|
||||
|
||||
$newIp4 = SiteFactory::normalize(array_diff($ip4, $this->ip4));
|
||||
$this->cidr4 = SiteFactory::normalize(IP4Helper::processCIDR($newIp4, $this->cidr4));
|
||||
|
||||
$newIp6 = SiteFactory::normalize(array_diff($ip6, $this->ip6));
|
||||
$this->cidr6 = SiteFactory::normalize(IP6Helper::processCIDR($newIp6, $this->cidr6));
|
||||
|
||||
$this->ip4 = SiteFactory::normalize(array_merge($this->ip4, $ip4));
|
||||
$this->ip6 = SiteFactory::normalize(array_merge($this->ip6, $ip6));
|
||||
|
||||
App::getLogger()->debug('Reloaded for ' . $this->name);
|
||||
|
||||
EventLoop::delay($this->timeout, function () {
|
||||
$this->reloadExternal();
|
||||
$this->reload();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
private function reloadExternal(): void {
|
||||
if (isset($this->external->domains)) {
|
||||
foreach ($this->external->domains as $url) {
|
||||
$this->domains = SiteFactory::normalize(
|
||||
array_merge($this->domains, explode("\n", file_get_contents($url)))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($this->external->ip4)) {
|
||||
foreach ($this->external->ip4 as $url) {
|
||||
$this->ip4 = SiteFactory::normalize(array_merge($this->ip4, explode("\n", file_get_contents($url))));
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($this->external->ip6)) {
|
||||
foreach ($this->external->ip6 as $url) {
|
||||
$this->ip6 = SiteFactory::normalize(array_merge($this->ip6, explode("\n", file_get_contents($url))));
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($this->external->cidr4)) {
|
||||
foreach ($this->external->cidr4 as $url) {
|
||||
$this->cidr4 = SiteFactory::normalize(
|
||||
array_merge($this->cidr4, explode("\n", file_get_contents($url)))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($this->external->cidr6)) {
|
||||
foreach ($this->external->cidr6 as $url) {
|
||||
$this->cidr6 = SiteFactory::normalize(
|
||||
array_merge($this->cidr6, explode("\n", file_get_contents($url)))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
App::getLogger()->debug('External reloaded for ' . $this->name);
|
||||
}
|
||||
}
|
77
src/Domain/Factory/SiteFactory.php
Normal file
77
src/Domain/Factory/SiteFactory.php
Normal file
|
@ -0,0 +1,77 @@
|
|||
<?php
|
||||
|
||||
namespace OpenCCK\Domain\Factory;
|
||||
|
||||
use OpenCCK\Domain\Entity\Site;
|
||||
use OpenCCK\Domain\Helper\IP4Helper;
|
||||
use OpenCCK\Domain\Helper\IP6Helper;
|
||||
use stdClass;
|
||||
|
||||
class SiteFactory {
|
||||
/**
|
||||
* @param string $name Name of portal
|
||||
* @param object $config Configuration of portal
|
||||
* @return Site
|
||||
*
|
||||
*/
|
||||
static function create(string $name, object $config): Site {
|
||||
$domains = $config->domains ?? [];
|
||||
$dns = $config->dns ?? [];
|
||||
$timeout = $config->timeout ?? 1440 * 60;
|
||||
$ip4 = $config->ip4 ?? [];
|
||||
$ip6 = $config->ip6 ?? [];
|
||||
$cidr4 = $config->cidr4 ?? [];
|
||||
$cidr6 = $config->cidr6 ?? [];
|
||||
$external = $config->external ?? new stdClass();
|
||||
|
||||
if (isset($external)) {
|
||||
if (isset($external->domains)) {
|
||||
foreach ($external->domains as $url) {
|
||||
$domains = array_merge($domains, explode("\n", file_get_contents($url)));
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($external->ip4)) {
|
||||
foreach ($external->ip4 as $url) {
|
||||
$ip4 = array_merge($ip4, explode("\n", file_get_contents($url)));
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($external->ip6)) {
|
||||
foreach ($external->ip6 as $url) {
|
||||
$ip6 = array_merge($ip6, explode("\n", file_get_contents($url)));
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($external->cidr4)) {
|
||||
foreach ($external->cidr4 as $url) {
|
||||
$cidr4 = array_merge($cidr4, explode("\n", file_get_contents($url)));
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($external->cidr6)) {
|
||||
foreach ($external->cidr6 as $url) {
|
||||
$cidr6 = array_merge($cidr6, explode("\n", file_get_contents($url)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$domains = self::normalize($domains);
|
||||
$ip4 = self::normalize($ip4);
|
||||
$ip6 = self::normalize($ip6);
|
||||
$cidr4 = self::normalize(IP4Helper::processCIDR($ip4, self::normalize($cidr4)));
|
||||
$cidr6 = self::normalize(IP6Helper::processCIDR($ip6, self::normalize($cidr6)));
|
||||
|
||||
return new Site($name, $domains, $dns, $timeout, $ip4, $ip6, $cidr4, $cidr6, $external);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $array
|
||||
* @return array
|
||||
*/
|
||||
public static function normalize(array $array): array {
|
||||
return array_values(
|
||||
array_unique(array_filter($array, fn(string $item) => !str_starts_with($item, '#') && strlen($item) > 0))
|
||||
);
|
||||
}
|
||||
}
|
69
src/Domain/Helper/DNSHelper.php
Normal file
69
src/Domain/Helper/DNSHelper.php
Normal file
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
namespace OpenCCK\Domain\Helper;
|
||||
|
||||
use Amp\Dns\DnsConfig;
|
||||
use Amp\Dns\DnsConfigLoader;
|
||||
use Amp\Dns\DnsException;
|
||||
use Amp\Dns\DnsRecord;
|
||||
use Amp\Dns\HostLoader;
|
||||
use Amp\Dns\Rfc1035StubDnsResolver;
|
||||
|
||||
use OpenCCK\Infrastructure\API\App;
|
||||
use function Amp\Dns\dnsResolver;
|
||||
use function Amp\Dns\resolve;
|
||||
|
||||
readonly class DNSHelper {
|
||||
public function __construct(private array $dnsServers = []) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $dnsServers
|
||||
* @return void
|
||||
*/
|
||||
private function setResolver(array $dnsServers): void {
|
||||
dnsResolver(
|
||||
new Rfc1035StubDnsResolver(
|
||||
null,
|
||||
new class ($dnsServers) implements DnsConfigLoader {
|
||||
public function __construct(private readonly array $dnsServers = []) {
|
||||
}
|
||||
|
||||
public function loadConfig(): DnsConfig {
|
||||
return new DnsConfig($this->dnsServers, (new HostLoader())->loadHosts());
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $domain
|
||||
* @return array[]
|
||||
*/
|
||||
public function resolve(string $domain): array {
|
||||
$ipv4 = [];
|
||||
$ipv6 = [];
|
||||
foreach ($this->dnsServers as $server) {
|
||||
$this->setResolver([$server]);
|
||||
try {
|
||||
$ipv4 = array_merge(
|
||||
$ipv4,
|
||||
array_map(fn(DnsRecord $record) => $record->getValue(), resolve($domain, DnsRecord::A))
|
||||
);
|
||||
} catch (DnsException $e) {
|
||||
App::getLogger()->error($e->getMessage(), [$server]);
|
||||
}
|
||||
try {
|
||||
$ipv6 = array_merge(
|
||||
$ipv6,
|
||||
array_map(fn(DnsRecord $record) => $record->getValue(), resolve($domain, DnsRecord::AAAA))
|
||||
);
|
||||
} catch (DnsException $e) {
|
||||
App::getLogger()->error($e->getMessage(), [$server]);
|
||||
}
|
||||
}
|
||||
App::getLogger()->debug('resolve: ' . $domain, [count($ipv4), count($ipv6)]);
|
||||
return [$ipv4, $ipv6];
|
||||
}
|
||||
}
|
144
src/Domain/Helper/IP4Helper.php
Normal file
144
src/Domain/Helper/IP4Helper.php
Normal file
|
@ -0,0 +1,144 @@
|
|||
<?php
|
||||
|
||||
namespace OpenCCK\Domain\Helper;
|
||||
|
||||
use OpenCCK\Infrastructure\API\App;
|
||||
use OpenCCK\Infrastructure\Storage\CIDRStorage;
|
||||
|
||||
use function Amp\async;
|
||||
use function Amp\delay;
|
||||
|
||||
class IP4Helper {
|
||||
public static function processCIDR(array $ips, $results = []): array {
|
||||
$count = count($ips);
|
||||
foreach ($ips as $i => $ip) {
|
||||
if ($ip === '127.0.0.1') {
|
||||
continue;
|
||||
}
|
||||
async(function () use ($ip, $i, $count, &$results) {
|
||||
if (CIDRStorage::getInstance()->has($ip)) {
|
||||
$searchArray = CIDRStorage::getInstance()->get($ip);
|
||||
$results = array_merge($results, self::trimCIDRs($searchArray));
|
||||
|
||||
App::getLogger()->debug($ip . ' -> ' . json_encode($searchArray), [$i + 1 . '/' . $count]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (self::isInRange($ip, $results)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$search = null;
|
||||
$result = shell_exec('whois ' . $ip . ' | grep CIDR');
|
||||
if ($result) {
|
||||
preg_match('/^CIDR:\s*(.*)$/m', $result, $matches);
|
||||
$search = $matches[1] ?? null;
|
||||
}
|
||||
|
||||
if (!$search) {
|
||||
$search = shell_exec(
|
||||
implode(' | ', [
|
||||
'whois -a ' . $ip,
|
||||
'grep inetnum',
|
||||
'head -n 1',
|
||||
"awk '{print $2\"-\"$4}'",
|
||||
'sed "s/-$//"',
|
||||
'xargs ipcalc',
|
||||
"grep -oE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/[0-9]+$'",
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
if (!$search) {
|
||||
$search = shell_exec(
|
||||
implode(' | ', [
|
||||
'whois -a ' . $ip,
|
||||
'grep IPv4',
|
||||
'grep " - "',
|
||||
'head -n 1',
|
||||
"awk '{print $3\"-\"$5}'",
|
||||
'xargs ipcalc',
|
||||
"grep -oE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/[0-9]+$'",
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
if ($search) {
|
||||
$search = strtr($search, ["\n" => ' ', ', ' => ' ']);
|
||||
$searchArray = array_filter(
|
||||
explode(' ', strtr($search, ' ', '')),
|
||||
fn(string $cidr) => strlen($cidr) > 0
|
||||
);
|
||||
CIDRStorage::getInstance()->set($ip, $searchArray);
|
||||
$results = array_merge($results, self::trimCIDRs($searchArray));
|
||||
|
||||
App::getLogger()->debug($ip . ' -> ' . json_encode($searchArray), [$i + 1 . '/' . $count]);
|
||||
} else {
|
||||
App::getLogger()->error($ip . ' -> CIDR not found', [$i + 1 . '/' . $count]);
|
||||
}
|
||||
delay(0.001);
|
||||
})->await();
|
||||
}
|
||||
|
||||
return self::minimizeSubnets($results);
|
||||
}
|
||||
|
||||
public static function trimCIDRs(array $searchArray): array {
|
||||
$subnets = [];
|
||||
foreach ($searchArray as $search) {
|
||||
foreach (explode(' ', $search) as $cidr) {
|
||||
if (str_contains($cidr, '/')) {
|
||||
$subnets[] = trim($cidr);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $subnets;
|
||||
}
|
||||
|
||||
public static function sortSubnets(array $subnets): array {
|
||||
usort($subnets, function ($a, $b) {
|
||||
return (int) explode('/', $a)[1] - (int) explode('/', $b)[1];
|
||||
});
|
||||
usort($subnets, function ($a, $b) {
|
||||
return ip2long(explode('/', $a)[0]) - ip2long(explode('/', $b)[0]);
|
||||
});
|
||||
return $subnets;
|
||||
}
|
||||
|
||||
public static function minimizeSubnets(array $subnets): array {
|
||||
$result = [];
|
||||
foreach (self::sortSubnets(array_filter($subnets, fn(string $subnet) => !!$subnet)) as $subnet) {
|
||||
$include = true;
|
||||
[$ip /*, $mask*/] = explode('/', $subnet);
|
||||
$ipLong = ip2long($ip);
|
||||
// $maskLong = ~((1 << 32 - (int) $mask) - 1);
|
||||
|
||||
foreach ($result as $resSubnet) {
|
||||
[$resIp, $resMask] = explode('/', $resSubnet);
|
||||
$resIpLong = ip2long($resIp);
|
||||
$resMaskLong = ~((1 << 32 - (int) $resMask) - 1);
|
||||
|
||||
if (($ipLong & $resMaskLong) === ($resIpLong & $resMaskLong)) {
|
||||
$include = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($include) {
|
||||
$result[] = $subnet;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public static function isInRange(string $ip, array $cidrs): bool {
|
||||
foreach ($cidrs as $cidr) {
|
||||
[$subnet, $mask] = explode('/', $cidr);
|
||||
if ((ip2long($ip) & ~((1 << 32 - $mask) - 1)) === ip2long($subnet)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
176
src/Domain/Helper/IP6Helper.php
Normal file
176
src/Domain/Helper/IP6Helper.php
Normal file
|
@ -0,0 +1,176 @@
|
|||
<?php
|
||||
|
||||
namespace OpenCCK\Domain\Helper;
|
||||
|
||||
use OpenCCK\Infrastructure\API\App;
|
||||
use OpenCCK\Infrastructure\Storage\CIDRStorage;
|
||||
|
||||
use function Amp\async;
|
||||
use function Amp\delay;
|
||||
|
||||
class IP6Helper {
|
||||
public static function processCIDR(array $ips, $results = []): array {
|
||||
$count = count($ips);
|
||||
foreach ($ips as $i => $ip) {
|
||||
if ($ip === '::1') {
|
||||
continue;
|
||||
}
|
||||
async(function () use ($ip, $i, $count, &$results) {
|
||||
if (CIDRStorage::getInstance()->has($ip)) {
|
||||
$searchArray = CIDRStorage::getInstance()->get($ip);
|
||||
$results = array_merge($results, self::trimCIDRs($searchArray));
|
||||
|
||||
App::getLogger()->debug($ip . ' -> ' . json_encode($searchArray), [$i + 1 . '/' . $count]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (self::isInRange($ip, $results)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$search = shell_exec(
|
||||
implode(' | ', [
|
||||
'whois -a ' . $ip,
|
||||
'grep inet6num',
|
||||
'grep -v "/0"',
|
||||
'head -n 1',
|
||||
"awk '{print $2}'",
|
||||
])
|
||||
);
|
||||
|
||||
if (!$search) {
|
||||
$search = shell_exec(
|
||||
implode(' | ', [
|
||||
'whois -a ' . $ip,
|
||||
'grep route6',
|
||||
'grep -v "/0"',
|
||||
'head -n 1',
|
||||
"awk '{print $2}'",
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
if ($search) {
|
||||
$search = strtr($search, ["\n" => ' ', ', ' => ' ']);
|
||||
$searchArray = array_filter(
|
||||
explode(' ', strtr($search, ' ', '')),
|
||||
fn(string $cidr) => strlen($cidr) > 0
|
||||
);
|
||||
CIDRStorage::getInstance()->set($ip, $searchArray);
|
||||
$results = array_merge($results, self::trimCIDRs($searchArray));
|
||||
|
||||
App::getLogger()->debug($ip . ' -> ' . json_encode($searchArray), [$i + 1 . '/' . $count]);
|
||||
} else {
|
||||
App::getLogger()->error($ip . ' -> CIDR not found', [$i + 1 . '/' . $count]);
|
||||
}
|
||||
delay(0.001);
|
||||
})->await();
|
||||
}
|
||||
|
||||
//return self::minimizeSubnets($results);
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $searchArray
|
||||
* @return array
|
||||
*/
|
||||
public static function trimCIDRs(array $searchArray): array {
|
||||
$subnets = [];
|
||||
foreach ($searchArray as $search) {
|
||||
foreach (explode(' ', $search) as $cidr) {
|
||||
if (str_contains($cidr, '/')) {
|
||||
[$address, $prefix] = explode('/', trim($cidr));
|
||||
$subnets[] = $address . '/' . max($prefix, 64);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $subnets;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $subnets
|
||||
* @return array
|
||||
*/
|
||||
public static function sortSubnets(array $subnets): array {
|
||||
usort($subnets, function ($a, $b) {
|
||||
return (int) explode('/', $a)[1] - (int) explode('/', $b)[1];
|
||||
});
|
||||
|
||||
usort($subnets, function ($a, $b) {
|
||||
$ipA = inet_pton(explode('/', $a)[0]);
|
||||
$ipB = inet_pton(explode('/', $b)[0]);
|
||||
|
||||
return strcmp($ipA, $ipB);
|
||||
});
|
||||
|
||||
return $subnets;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $subnets
|
||||
* @return array
|
||||
*/
|
||||
public static function minimizeSubnets(array $subnets): array {
|
||||
$result = [];
|
||||
foreach (self::sortSubnets(array_filter($subnets, fn(string $subnet) => !!$subnet)) as $subnet) {
|
||||
if (!$subnet) {
|
||||
continue;
|
||||
}
|
||||
[$address, $prefix] = explode('/', $subnet);
|
||||
|
||||
$addressNum = inet_pton($address);
|
||||
$addressNum = unpack('J', $addressNum)[1];
|
||||
|
||||
$isUnique = true;
|
||||
foreach ($result as $existingCidr) {
|
||||
[$existingAddress, $existingPrefix] = explode('/', $existingCidr);
|
||||
$existingAddressNum = inet_pton($existingAddress);
|
||||
$existingAddressNum = unpack('J', $existingAddressNum)[1];
|
||||
|
||||
$mask = (1 << 128) - (1 << 128 - $prefix);
|
||||
$existingMask = (1 << 128) - (1 << 128 - $existingPrefix);
|
||||
if (($addressNum & $mask) === ($existingAddressNum & $existingMask)) {
|
||||
$isUnique = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($isUnique) {
|
||||
$result[] = $subnet;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public static function isInRange(string $ip, array $cidrs): bool {
|
||||
$ip = inet_pton($ip);
|
||||
|
||||
foreach ($cidrs as $cidr) {
|
||||
[$subnet, $mask] = explode('/', $cidr);
|
||||
$subnet = inet_pton($subnet);
|
||||
|
||||
$mask = intval($mask);
|
||||
$binaryMask = str_repeat('f', $mask >> 2);
|
||||
switch ($mask % 4) {
|
||||
case 1:
|
||||
$binaryMask .= '8';
|
||||
break;
|
||||
case 2:
|
||||
$binaryMask .= 'c';
|
||||
break;
|
||||
case 3:
|
||||
$binaryMask .= 'e';
|
||||
break;
|
||||
}
|
||||
$binaryMask = str_pad($binaryMask, 32, '0');
|
||||
$mask = pack('H*', $binaryMask);
|
||||
|
||||
if (($ip & $mask) === ($subnet & $mask)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
107
src/Infrastructure/API/App.php
Normal file
107
src/Infrastructure/API/App.php
Normal file
|
@ -0,0 +1,107 @@
|
|||
<?php
|
||||
|
||||
namespace OpenCCK\Infrastructure\API;
|
||||
|
||||
use Amp\ByteStream\WritableResourceStream;
|
||||
use Amp\Log\ConsoleFormatter;
|
||||
use Amp\Log\StreamHandler;
|
||||
|
||||
use Revolt\EventLoop;
|
||||
use Revolt\EventLoop\UnsupportedFeatureException;
|
||||
|
||||
use Closure;
|
||||
use Dotenv\Dotenv;
|
||||
use Monolog\Logger;
|
||||
use Psr\Log\LogLevel;
|
||||
|
||||
use function Amp\trapSignal;
|
||||
use function OpenCCK\getEnv;
|
||||
use function sprintf;
|
||||
|
||||
final class App {
|
||||
private static App $_instance;
|
||||
|
||||
/**
|
||||
* @param array<AppModuleInterface> $modules
|
||||
*/
|
||||
private array $modules = [];
|
||||
|
||||
private bool $isEventLoopStarted = false;
|
||||
|
||||
/**
|
||||
* @param ?Logger $logger
|
||||
*/
|
||||
private function __construct(private ?Logger $logger = null) {
|
||||
ini_set('memory_limit', getEnv('SYS_MEMORY_LIMIT') ?? '2048M');
|
||||
|
||||
if (!defined('PATH_ROOT')) {
|
||||
define('PATH_ROOT', dirname(__DIR__, 3));
|
||||
}
|
||||
|
||||
$dotenv = Dotenv::createImmutable(PATH_ROOT);
|
||||
$dotenv->safeLoad();
|
||||
|
||||
if ($timezone = getEnv('SYS_TIMEZONE')) {
|
||||
date_default_timezone_set($timezone);
|
||||
}
|
||||
|
||||
$this->logger = $logger ?? new Logger(getEnv('COMPOSE_PROJECT_NAME') ?? 'iplist');
|
||||
$logHandler = new StreamHandler(new WritableResourceStream(STDOUT));
|
||||
$logHandler->setFormatter(new ConsoleFormatter());
|
||||
$logHandler->setLevel(getEnv('DEBUG') === 'false' ? LogLevel::INFO : LogLevel::DEBUG);
|
||||
$this->logger->pushHandler($logHandler);
|
||||
|
||||
EventLoop::setErrorHandler(function ($e) {
|
||||
$this->logger->error($e->getMessage());
|
||||
});
|
||||
}
|
||||
|
||||
public static function getInstance(?Logger $logger = null): self {
|
||||
return self::$_instance ??= new self($logger);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Closure<AppModuleInterface> $handler
|
||||
* @return $this
|
||||
*/
|
||||
public function addModule(Closure $handler): self {
|
||||
$module = $handler($this);
|
||||
$this->modules[$module::class] = $module;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getModules(): array {
|
||||
return $this->modules;
|
||||
}
|
||||
|
||||
public static function getLogger(): ?Logger {
|
||||
return self::$_instance->logger;
|
||||
}
|
||||
|
||||
public function start(): void {
|
||||
foreach ($this->getModules() as $module) {
|
||||
$module->start();
|
||||
}
|
||||
if (defined('SIGINT') && defined('SIGTERM')) {
|
||||
// Await SIGINT or SIGTERM to be received.
|
||||
try {
|
||||
$signal = trapSignal([SIGINT, SIGTERM]);
|
||||
$this->logger->info(sprintf('Received signal %d, stopping server', $signal));
|
||||
} catch (UnsupportedFeatureException $e) {
|
||||
$this->logger->error($e->getMessage());
|
||||
}
|
||||
$this->stop();
|
||||
} else {
|
||||
if (!$this->isEventLoopStarted && !defined('PHPUNIT_COMPOSER_INSTALL')) {
|
||||
$this->isEventLoopStarted = true;
|
||||
EventLoop::run();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function stop(): void {
|
||||
foreach ($this->modules as $module) {
|
||||
$module->stop();
|
||||
}
|
||||
}
|
||||
}
|
8
src/Infrastructure/API/AppModuleInterface.php
Normal file
8
src/Infrastructure/API/AppModuleInterface.php
Normal file
|
@ -0,0 +1,8 @@
|
|||
<?php
|
||||
|
||||
namespace OpenCCK\Infrastructure\API;
|
||||
|
||||
interface AppModuleInterface {
|
||||
public function start(): void;
|
||||
public function stop(): void;
|
||||
}
|
17
src/Infrastructure/API/Handler/HTTPErrorHandler.php
Normal file
17
src/Infrastructure/API/Handler/HTTPErrorHandler.php
Normal file
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
|
||||
namespace OpenCCK\Infrastructure\API\Handler;
|
||||
|
||||
use Amp\Http\Server\ErrorHandler;
|
||||
use Amp\Http\Server\Request;
|
||||
use Amp\Http\Server\Response;
|
||||
|
||||
final class HTTPErrorHandler implements ErrorHandler {
|
||||
public function handleError(int $status, ?string $reason = null, ?Request $request = null): Response {
|
||||
return new Response(
|
||||
status: $status,
|
||||
headers: ['content-type' => 'application/json; charset=utf-8'],
|
||||
body: json_encode(['message' => $reason, 'code' => $status])
|
||||
);
|
||||
}
|
||||
}
|
58
src/Infrastructure/API/Handler/HTTPHandler.php
Normal file
58
src/Infrastructure/API/Handler/HTTPHandler.php
Normal file
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
namespace OpenCCK\Infrastructure\API\Handler;
|
||||
|
||||
use Amp\Http\Server\Request;
|
||||
use Amp\Http\Server\RequestHandler;
|
||||
use Amp\Http\Server\RequestHandler\ClosureRequestHandler;
|
||||
use Amp\Http\Server\Response;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Throwable;
|
||||
|
||||
use function OpenCCK\getEnv;
|
||||
|
||||
final class HTTPHandler extends Handler implements HTTPHandlerInterface {
|
||||
private function __construct(private readonly LoggerInterface $logger, array $headers = null) {
|
||||
}
|
||||
|
||||
public static function getInstance(LoggerInterface $logger, array $headers = null): HTTPHandler {
|
||||
return new self($logger, $headers);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $controllerName
|
||||
* @return RequestHandler
|
||||
*/
|
||||
public function getHandler(string $controllerName = 'main'): RequestHandler {
|
||||
return new ClosureRequestHandler(function (Request $request) use ($controllerName): Response {
|
||||
try {
|
||||
$response = $this->getController(
|
||||
ucfirst($request->getQueryParameter('format') ?: $controllerName),
|
||||
$request,
|
||||
$this->headers ?? []
|
||||
)();
|
||||
} catch (Throwable $e) {
|
||||
$this->logger->warning('Exception', [
|
||||
'exception' => $e::class,
|
||||
'error' => $e->getMessage(),
|
||||
'code' => $e->getCode(),
|
||||
]);
|
||||
$response = new Response(
|
||||
status: $e->getCode() ?: 500,
|
||||
headers: $this->headers ?? ['content-type' => 'application/json; charset=utf-8'],
|
||||
body: json_encode(
|
||||
array_merge(
|
||||
['message' => $e->getMessage(), 'code' => $e->getCode()],
|
||||
getEnv('DEBUG') === 'true'
|
||||
? ['file' => $e->getFile() . ':' . $e->getLine(), 'trace' => $e->getTrace()]
|
||||
: []
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $response;
|
||||
});
|
||||
}
|
||||
}
|
12
src/Infrastructure/API/Handler/HTTPHandlerInterface.php
Normal file
12
src/Infrastructure/API/Handler/HTTPHandlerInterface.php
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
|
||||
namespace OpenCCK\Infrastructure\API\Handler;
|
||||
|
||||
use Amp\Http\Server\RequestHandler;
|
||||
|
||||
interface HTTPHandlerInterface extends HandlerInterface {
|
||||
/**
|
||||
* @return RequestHandler
|
||||
*/
|
||||
public function getHandler(): RequestHandler;
|
||||
}
|
27
src/Infrastructure/API/Handler/Handler.php
Normal file
27
src/Infrastructure/API/Handler/Handler.php
Normal file
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
namespace OpenCCK\Infrastructure\API\Handler;
|
||||
|
||||
use Amp\Http\HttpStatus;
|
||||
use Amp\Http\Server\Request;
|
||||
|
||||
use OpenCCK\App\Controller\AbstractController;
|
||||
|
||||
use Exception;
|
||||
|
||||
abstract class Handler implements HandlerInterface {
|
||||
/**
|
||||
* @param string $name
|
||||
* @param Request $request
|
||||
* @param ?string[] $headers
|
||||
* @return AbstractController
|
||||
* @throws Exception
|
||||
*/
|
||||
protected function getController(string $name, Request $request, array $headers = null): AbstractController {
|
||||
$className = '\\OpenCCK\\App\\Controller\\' . ucfirst($name) . 'Controller';
|
||||
if (!class_exists($className)) {
|
||||
throw new Exception('Controller ' . $className . ' not found', HttpStatus::NOT_FOUND);
|
||||
}
|
||||
return new $className($request, $headers ?? ['content-type' => 'text/plain']);
|
||||
}
|
||||
}
|
12
src/Infrastructure/API/Handler/HandlerInterface.php
Normal file
12
src/Infrastructure/API/Handler/HandlerInterface.php
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
|
||||
namespace OpenCCK\Infrastructure\API\Handler;
|
||||
|
||||
use Amp\Http\Server\RequestHandler;
|
||||
|
||||
interface HandlerInterface {
|
||||
/**
|
||||
* @return RequestHandler
|
||||
*/
|
||||
public function getHandler(): RequestHandler;
|
||||
}
|
119
src/Infrastructure/API/Server.php
Normal file
119
src/Infrastructure/API/Server.php
Normal file
|
@ -0,0 +1,119 @@
|
|||
<?php
|
||||
|
||||
namespace OpenCCK\Infrastructure\API;
|
||||
|
||||
use Amp\ByteStream\BufferException;
|
||||
use Amp\Http\Server\DefaultErrorHandler;
|
||||
use Amp\Http\Server\Driver\ConnectionLimitingClientFactory;
|
||||
use Amp\Http\Server\Driver\ConnectionLimitingServerSocketFactory;
|
||||
use Amp\Http\Server\Driver\DefaultHttpDriverFactory;
|
||||
use Amp\Http\Server\Driver\SocketClientFactory;
|
||||
use Amp\Http\Server\ErrorHandler;
|
||||
use Amp\Http\Server\HttpServer;
|
||||
use Amp\Http\Server\HttpServerStatus;
|
||||
use Amp\Http\Server\SocketHttpServer;
|
||||
use Amp\Socket\BindContext;
|
||||
use Amp\Sync\LocalSemaphore;
|
||||
use Amp\Socket;
|
||||
|
||||
use Monolog\Logger;
|
||||
use OpenCCK\App\Service\IPListService;
|
||||
use OpenCCK\Infrastructure\API\Handler\HTTPHandler;
|
||||
use Throwable;
|
||||
|
||||
use function OpenCCK\getEnv;
|
||||
|
||||
final class Server implements AppModuleInterface {
|
||||
private static Server $_instance;
|
||||
|
||||
private int $connectionLimit = 1024;
|
||||
private int $connectionPerIpLimit = 10;
|
||||
|
||||
/**
|
||||
* @param ?HttpServer $httpServer
|
||||
* @param ?ErrorHandler $errorHandler
|
||||
* @param ?BindContext $bindContext
|
||||
* @param ?Logger $logger
|
||||
* @throws Throwable
|
||||
*/
|
||||
private function __construct(
|
||||
private ?HttpServer $httpServer,
|
||||
private ?ErrorHandler $errorHandler,
|
||||
private ?Socket\BindContext $bindContext,
|
||||
private ?Logger $logger
|
||||
) {
|
||||
$this->logger = $logger ?? App::getLogger();
|
||||
$serverSocketFactory = new ConnectionLimitingServerSocketFactory(new LocalSemaphore($this->connectionLimit));
|
||||
$clientFactory = new ConnectionLimitingClientFactory(
|
||||
new SocketClientFactory($this->logger),
|
||||
$this->logger,
|
||||
$this->connectionPerIpLimit
|
||||
);
|
||||
$this->httpServer =
|
||||
$httpServer ??
|
||||
new SocketHttpServer(
|
||||
logger: $this->logger,
|
||||
serverSocketFactory: $serverSocketFactory,
|
||||
clientFactory: $clientFactory,
|
||||
httpDriverFactory: new DefaultHttpDriverFactory(logger: $this->logger, streamTimeout: 60)
|
||||
);
|
||||
$this->bindContext = $bindContext ?? (new Socket\BindContext())->withoutTlsContext();
|
||||
$this->errorHandler = $errorHandler ?? new DefaultErrorHandler();
|
||||
|
||||
// инициализация сервиса
|
||||
IPListService::getInstance($this->logger);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ?HttpServer $httpServer
|
||||
* @param ?ErrorHandler $errorHandler
|
||||
* @param ?BindContext $bindContext
|
||||
* @param ?Logger $logger
|
||||
* @throws BufferException
|
||||
* @throws Throwable
|
||||
*/
|
||||
public static function getInstance(
|
||||
HttpServer $httpServer = null,
|
||||
ErrorHandler $errorHandler = null,
|
||||
Socket\BindContext $bindContext = null,
|
||||
Logger $logger = null
|
||||
): Server {
|
||||
return self::$_instance ??= new self($httpServer, $errorHandler, $bindContext, $logger);
|
||||
}
|
||||
|
||||
/**
|
||||
* Запуск веб-сервера
|
||||
* @return void
|
||||
*/
|
||||
public function start(): void {
|
||||
try {
|
||||
$this->httpServer->expose(
|
||||
new Socket\InternetAddress(getEnv('HTTP_HOST') ?? '0.0.0.0', getEnv('HTTP_PORT') ?? 8080),
|
||||
$this->bindContext
|
||||
);
|
||||
//$this->socketHttpServer->expose(
|
||||
// new Socket\InternetAddress('[::]', $_ENV['HTTP_PORT'] ?? 8080),
|
||||
// $this->bindContext
|
||||
//);
|
||||
$this->httpServer->start(HTTPHandler::getInstance($this->logger)->getHandler(), $this->errorHandler);
|
||||
} catch (Socket\SocketException $e) {
|
||||
$this->logger->warning($e->getMessage());
|
||||
} catch (Throwable $e) {
|
||||
$this->logger->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function stop(): void {
|
||||
$this->httpServer->stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return HttpServerStatus
|
||||
*/
|
||||
public function getStatus(): HttpServerStatus {
|
||||
return $this->httpServer->getStatus();
|
||||
}
|
||||
}
|
44
src/Infrastructure/Storage/CIDRStorage.php
Normal file
44
src/Infrastructure/Storage/CIDRStorage.php
Normal file
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
namespace OpenCCK\Infrastructure\Storage;
|
||||
|
||||
use OpenCCK\Infrastructure\API\App;
|
||||
use Revolt\EventLoop;
|
||||
|
||||
class CIDRStorage implements StorageInterface {
|
||||
const FILENAME = 'cidr.json';
|
||||
|
||||
private static CIDRStorage $_instance;
|
||||
private array $data = [];
|
||||
|
||||
private function __construct() {
|
||||
$path = PATH_ROOT . '/storage/' . self::FILENAME;
|
||||
if (is_file($path)) {
|
||||
$this->data = (array) json_decode(file_get_contents($path)) ?? [];
|
||||
}
|
||||
|
||||
EventLoop::repeat(\OpenCCK\getEnv('STORAGE_SAVE_INTERVAL') ?? 120, $this->save(...));
|
||||
}
|
||||
|
||||
public static function getInstance(): CIDRStorage {
|
||||
return self::$_instance ??= new self();
|
||||
}
|
||||
|
||||
public function get(string $key): ?array {
|
||||
return $this->data[$key] ?? null;
|
||||
}
|
||||
|
||||
public function set(string $key, mixed $value): bool {
|
||||
$this->data[$key] = $value;
|
||||
return true;
|
||||
}
|
||||
|
||||
public function has(string $key): bool {
|
||||
return isset($this->data[$key]);
|
||||
}
|
||||
|
||||
private function save(): void {
|
||||
file_put_contents(PATH_ROOT . '/storage/' . self::FILENAME, json_encode($this->data, JSON_PRETTY_PRINT));
|
||||
App::getLogger()->notice('Whois storage saved', [count($this->data) . ' items']);
|
||||
}
|
||||
}
|
9
src/Infrastructure/Storage/StorageInterface.php
Normal file
9
src/Infrastructure/Storage/StorageInterface.php
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
namespace OpenCCK\Infrastructure\Storage;
|
||||
|
||||
interface StorageInterface {
|
||||
public function get(string $key): mixed;
|
||||
public function set(string $key, mixed $value): bool;
|
||||
public function has(string $key): bool;
|
||||
}
|
20
src/Infrastructure/Task/ShellTask.php
Normal file
20
src/Infrastructure/Task/ShellTask.php
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
namespace OpenCCK\Infrastructure\Task;
|
||||
|
||||
use Amp\Cancellation;
|
||||
use Amp\Sync\Channel;
|
||||
|
||||
readonly class ShellTask implements TaskInterface {
|
||||
public function __construct(private string $command) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Channel $channel
|
||||
* @param Cancellation $cancellation
|
||||
* @return string
|
||||
*/
|
||||
public function run(Channel $channel, Cancellation $cancellation): mixed {
|
||||
return shell_exec($this->command);
|
||||
}
|
||||
}
|
11
src/Infrastructure/Task/TaskInterface.php
Normal file
11
src/Infrastructure/Task/TaskInterface.php
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace OpenCCK\Infrastructure\Task;
|
||||
|
||||
use Amp\Cancellation;
|
||||
use Amp\Parallel\Worker\Task;
|
||||
use Amp\Sync\Channel;
|
||||
|
||||
interface TaskInterface extends Task {
|
||||
public function run(Channel $channel, Cancellation $cancellation): mixed;
|
||||
}
|
33
src/functions.php
Normal file
33
src/functions.php
Normal file
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
namespace OpenCCK;
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
* @return ?string
|
||||
*/
|
||||
function getEnv(string $key): ?string {
|
||||
if (!isset($_ENV[$key])) {
|
||||
return null;
|
||||
}
|
||||
if (preg_match('/^"(.*)"$/i', $_ENV[$key], $matches)) {
|
||||
return $matches[1];
|
||||
}
|
||||
return $_ENV[$key];
|
||||
}
|
||||
|
||||
/**
|
||||
* Debug function
|
||||
* @param mixed $mixed
|
||||
* @param bool $exit
|
||||
* @return void
|
||||
*/
|
||||
function dbg(mixed $mixed, bool $exit = true): void {
|
||||
if (php_sapi_name() == 'cli') {
|
||||
echo print_r($mixed, true);
|
||||
} else {
|
||||
echo '<pre>' . print_r($mixed, true) . '</pre>';
|
||||
}
|
||||
if ($exit) {
|
||||
exit();
|
||||
}
|
||||
}
|
1935
storage/cidr.json
Normal file
1935
storage/cidr.json
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue