private fork from github
This commit is contained in:
commit
ddcacca5c3
37 changed files with 5748 additions and 0 deletions
10
.env.example
Normal file
10
.env.example
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
path=""
|
||||||
|
CONTAINER_NAME="pf"
|
||||||
|
DOMAIN=""
|
||||||
|
SERVER_NAME=""
|
||||||
|
APP_PASSWORD=""
|
||||||
|
MYSQL_PASSWORD=""
|
||||||
|
#API KEYS
|
||||||
|
CCP_SSO_CLIENT_ID=""
|
||||||
|
CCP_SSO_SECRET_KEY=""
|
||||||
|
CCP_ESI_SCOPES="esi-location.read_online.v1,esi-location.read_location.v1,esi-location.read_ship_type.v1,esi-ui.write_waypoint.v1,esi-ui.open_window.v1,esi-universe.read_structures.v1,esi-corporations.read_corporation_membership.v1,esi-clones.read_clones.v1"
|
||||||
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
.env
|
||||||
6
.gitmodules
vendored
Normal file
6
.gitmodules
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
[submodule "pathfinder"]
|
||||||
|
path = pathfinder
|
||||||
|
url = https://github.com/samoneilll/pathfinder
|
||||||
|
[submodule "websocket"]
|
||||||
|
path = websocket
|
||||||
|
url = https://github.com/exodus4d/pathfinder_websocket
|
||||||
46
Dockerfile
Normal file
46
Dockerfile
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
FROM php:7.2.5-fpm-alpine3.7 as build
|
||||||
|
#FROM php:7.4.4-fpm-alpine3.11 as build
|
||||||
|
RUN apk update \
|
||||||
|
&& apk add --no-cache libpng-dev zeromq-dev git \
|
||||||
|
$PHPIZE_DEPS \
|
||||||
|
&& docker-php-ext-install gd && docker-php-ext-install pdo_mysql && pecl install redis && docker-php-ext-enable redis && pecl install channel://pecl.php.net/zmq-1.1.3 && docker-php-ext-enable zmq \
|
||||||
|
&& curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
|
||||||
|
|
||||||
|
COPY pathfinder /app
|
||||||
|
WORKDIR /app
|
||||||
|
RUN composer install
|
||||||
|
|
||||||
|
#FROM trafex/alpine-nginx-php7:latest
|
||||||
|
FROM trafex/alpine-nginx-php7:ba1dd422
|
||||||
|
#USER root
|
||||||
|
RUN apk update && apk add --no-cache busybox-suid sudo php7-redis php7-pdo php7-pdo_mysql php7-fileinfo shadow gettext bash apache2-utils
|
||||||
|
#RUN usermod -u 1000 nobody
|
||||||
|
#RUN groupmod -g 1000 nobody
|
||||||
|
|
||||||
|
COPY static/nginx/nginx.conf /etc/nginx/templateNginx.conf
|
||||||
|
COPY static/nginx/site.conf /etc/nginx/sites_enabled/templateSite.conf
|
||||||
|
|
||||||
|
# Configure PHP-FPM
|
||||||
|
COPY static/php/fpm-pool.conf /etc/php7/php-fpm.d/zzz_custom.conf
|
||||||
|
#COPY static/php/php.ini /etc/php7/conf.d/zzz_custom.ini
|
||||||
|
|
||||||
|
COPY static/php/php.ini /etc/zzz_custom.ini
|
||||||
|
# configure cron
|
||||||
|
COPY static/crontab.txt /var/crontab.txt
|
||||||
|
# Configure supervisord
|
||||||
|
COPY static/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||||
|
COPY static/entrypoint.sh /
|
||||||
|
#RUN apk add sudo php7-redis php7-pdo php7-pdo_mysql
|
||||||
|
WORKDIR /var/www/html
|
||||||
|
COPY --chown=nobody --from=build /app pathfinder
|
||||||
|
##RUN chown -R nobody:nobody /var/www/html/pathfinder
|
||||||
|
|
||||||
|
RUN chmod 0766 pathfinder/logs pathfinder/tmp/ && rm index.php && touch /etc/nginx/.setup_pass && chmod +x /entrypoint.sh
|
||||||
|
COPY static/pathfinder/routes.ini /var/www/html/pathfinder/app/
|
||||||
|
COPY static/pathfinder/environment.ini /var/www/html/pathfinder/app/templateEnvironment.ini
|
||||||
|
#USER nobody
|
||||||
|
WORKDIR /var/www/html
|
||||||
|
EXPOSE 80
|
||||||
|
#ENTRYPOINT ["sh", "-c", "source /.env && /entrypoint.sh", "-s"]
|
||||||
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
|
CMD ["/usr/bin/supervisord", "-c", "/etc/supervisor/conf.d/supervisord.conf"]
|
||||||
24
LICENSE.md
Normal file
24
LICENSE.md
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
THE FOLLOWING SETS FORTH ATTRIBUTION NOTICES FOR THIRD PARTY SOFTWARE THAT MAY BE CONTAINED IN PORTIONS OF THE PATHFINDER CONTAINER PRODUCT.
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2020-present techfreak and other contributors
|
||||||
|
|
||||||
|
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.
|
||||||
71
README.md
Normal file
71
README.md
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
# Pathfinder Container
|
||||||
|
|
||||||
|
**Pathfinder Container** is a docker-compose setup that contains a hassle free out of the box setup for [Pathfinder](https://developers.eveonline.com/https://github.com/exodus4d/pathfinder).
|
||||||
|
|
||||||
|
**Features**
|
||||||
|
* Setup Script for easy setup
|
||||||
|
* Password Protection of the setup page
|
||||||
|
* Socket Server running out of the box
|
||||||
|
* Automatic Restart in-case of crash
|
||||||
|
* Easy update with git tags
|
||||||
|
### How to run it
|
||||||
|
|
||||||
|
**Prerequisites**:
|
||||||
|
* [docker](https://docs.docker.com/)
|
||||||
|
* [docker-compose](https://docs.docker.com/)
|
||||||
|
|
||||||
|
1. **Create an [API-Key](https://developers.eveonline.com/) with the scopes listed in the [wiki](https://github.com/exodus4d/pathfinder/wiki/SSO-ESI)**
|
||||||
|
|
||||||
|
2. **Clone the repo**
|
||||||
|
```shell
|
||||||
|
git clone --recurse-submodules https://gitlab.com/techfreak/pathfinder-container
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setup Script
|
||||||
|
3. **Run the setup script**
|
||||||
|
```shell
|
||||||
|
chmod +x setup.sh
|
||||||
|
./setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Profit ! Connect it to nginx or let traefik discover it**
|
||||||
|
## Running it manually
|
||||||
|
3. **Edit the .env file and make sure every config option has an entry.**
|
||||||
|
```shell
|
||||||
|
#the folder path of this folder e.g /home/tech/Development/DOCKER/pathfinder-container
|
||||||
|
path=""
|
||||||
|
CONTAINER_NAME="pf"
|
||||||
|
DOMAIN=""
|
||||||
|
SERVER_NAME=""
|
||||||
|
APP_PASSWORD=""
|
||||||
|
MYSQL_PASSWORD=""
|
||||||
|
CCP_SSO_CLIENT_ID=""
|
||||||
|
CCP_SSO_SECRET_KEY=""
|
||||||
|
CCP_ESI_SCOPES="esi-location.read_online.v1,esi-location.read_location.v1,esi-location.read_ship_type.v1,esi-ui.write_waypoint.v1,esi-ui.open_window.v1,esi-universe.read_structures.v1,esi-corporations.read_corporation_membership.v1,esi-clones.read_clones.v1"
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **Build & Run it**
|
||||||
|
```shell
|
||||||
|
docker-compose build && docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Open the http://< your-domain >/setup page. Your username is pf and password is the password you set in APP_PASSWORD. Click on create database for eve_universe and pathfinder. And click on setup tables && fix column/keys.**
|
||||||
|
|
||||||
|
|
||||||
|
6. **Go back to your console and insert the eve universe dump with this command **
|
||||||
|
```shell
|
||||||
|
docker-compose exec pfdb /bin/sh -c "unzip -p eve_universe.sql.zip | mysql -u root -p\$MYSQL_ROOT_PASSWORD eve_universe";
|
||||||
|
```
|
||||||
|
|
||||||
|
7. **Profit ! Connect it to nginx or let traefik discover it**
|
||||||
|
|
||||||
|
### Acknowledgments
|
||||||
|
* [exodus4d](https://github.com/exodus4d/) for pathfinder
|
||||||
|
* [Markus Geiger](https://gist.github.com/blurayne/f63c5a8521c0eeab8e9afd8baa45c65e) for his awesome bash menu
|
||||||
|
|
||||||
|
### Authors
|
||||||
|
* techfreak
|
||||||
|
|
||||||
|
### License
|
||||||
|
This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details
|
||||||
|
|
||||||
144
config/pathfinder/config.ini
Normal file
144
config/pathfinder/config.ini
Normal file
|
|
@ -0,0 +1,144 @@
|
||||||
|
; Global Framework Config
|
||||||
|
|
||||||
|
[SERVER]
|
||||||
|
SERVER_NAME = CARLFINDER
|
||||||
|
|
||||||
|
[globals]
|
||||||
|
; Verbosity level of error stack trace for errors
|
||||||
|
; This affects error logging and stack traces returned to clients on error.
|
||||||
|
; DEBUG level can be overwritten in environment.ini
|
||||||
|
; Syntax: 0 | 1 | 2 | 3
|
||||||
|
; Default: 0
|
||||||
|
DEBUG = 0
|
||||||
|
|
||||||
|
; How to behave on 'non-fatal' errors
|
||||||
|
; If TRUE, the framework, after having logged stack trace and errors, stops execution
|
||||||
|
; (die without any status) when a non-fatal error is detected.
|
||||||
|
; Hint: You should not change this.
|
||||||
|
; Syntax: TRUE | FALSE
|
||||||
|
; Default: FALSE
|
||||||
|
HALT = FALSE
|
||||||
|
|
||||||
|
; Timezone to use
|
||||||
|
; Sync Pathfinder with EVE server time.
|
||||||
|
; Hint: You should not change this.
|
||||||
|
; Default: UTC
|
||||||
|
TZ = UTC
|
||||||
|
|
||||||
|
; Default language
|
||||||
|
; Overwrites HTTP Accept-Language request header.
|
||||||
|
; Used by setlocale() and affects number formatting.
|
||||||
|
; Syntax: String
|
||||||
|
; Default: en-US
|
||||||
|
LANGUAGE = en-US
|
||||||
|
|
||||||
|
; Cache key prefix
|
||||||
|
; Same for all cache values for this installation.
|
||||||
|
; CLI (cronjob) scripts use it for cache manipulation.
|
||||||
|
; Hint: You should not change this.
|
||||||
|
; Syntax String
|
||||||
|
; Default: {{ md5(@SERVER.SERVER_NAME) }}
|
||||||
|
SEED = {{ md5(@SERVER.SERVER_NAME) }}
|
||||||
|
|
||||||
|
; Cache backend
|
||||||
|
; This sets the primary cache backend for Pathfinder. Used for e.g.:
|
||||||
|
; DB query, DB schema, HTTP response, or even simple key->value caches
|
||||||
|
; Can handle Redis, Memcache module, APC, WinCache, XCache and a filesystem-based cache.
|
||||||
|
; Hint: Redis is recommended and gives the best performance.
|
||||||
|
; Syntax: folder=[DIR] | redis=[SERVER]
|
||||||
|
; Default: folder=tmp/cache/
|
||||||
|
; Value: FALSE
|
||||||
|
; - Disables caching
|
||||||
|
; folder=[DIR]
|
||||||
|
; - Cache data is stored on disc
|
||||||
|
; redis=[SERVER]
|
||||||
|
; - Cache data is stored in Redis. redis=[host]:[port]:[db]:[auth] (e.g. redis=localhost:6379:1:myPass)
|
||||||
|
CACHE = redis=${CONTAINER_NAME}-redis:6379
|
||||||
|
|
||||||
|
; Cache backend for API data
|
||||||
|
; This sets the cache backend for API response data and other temp data relates to API requests.
|
||||||
|
; Response data with proper 'Expire' HTTP Header will be cached here and speed up further requests.
|
||||||
|
; As default 'API_CACHE' and 'CACHE' share the same backend (cache location)
|
||||||
|
; Hint1: You can specify e.g. a dedicated Redis DB here, then 'CACHE' and 'API_CACHE' can be cleared independently
|
||||||
|
; Hint2: Redis is recommended and gives the best performance.
|
||||||
|
; Default: {{@CACHE}}
|
||||||
|
; Value: FALSE
|
||||||
|
; - Disables caching
|
||||||
|
; folder=[DIR]
|
||||||
|
; - Cache data is stored on disc
|
||||||
|
; redis=[SERVER]
|
||||||
|
; - Cache data is stored in Redis. redis=[host]:[port]:[db]:[auth] (e.g. redis=localhost:6379:2:myPass)
|
||||||
|
API_CACHE = {{@CACHE}}
|
||||||
|
|
||||||
|
; Cache backend used by PHPs Session handler.
|
||||||
|
; Hint1: Best performance and recommended configuration for Pathfinder is to configured Redis as PHPs default Session handler
|
||||||
|
; in your php.ini and set 'default' value here in order to use Redis (fastest)
|
||||||
|
; Hint2: If Redis is not available for you, leave this at 'mysql' (faster than PHPs default files bases Sessions)
|
||||||
|
; Syntax: mysql | default
|
||||||
|
; Default: mysql
|
||||||
|
; Value: mysql
|
||||||
|
; - Session data get stored in 'pathfinder'.'sessions' table (environment.ini → DB_PF_NAME).
|
||||||
|
; Table `sessions` is auto created if not exist.
|
||||||
|
; default
|
||||||
|
; - Session data get stored in PHPs default Session handler (php.ini → session.save_handler and session.save_path)
|
||||||
|
; PHPs default session.save_handler is `files` and each Session is written to disc (slowest)
|
||||||
|
SESSION_CACHE = mysql
|
||||||
|
|
||||||
|
; Callback functions ==============================================================================
|
||||||
|
ONERROR = {{ @NAMESPACE }}\Controller\Controller->showError
|
||||||
|
UNLOAD = {{ @NAMESPACE }}\Controller\Controller->unload
|
||||||
|
|
||||||
|
; Path configurations =============================================================================
|
||||||
|
; All path configurations are relative to BASE dir and should NOT be changed
|
||||||
|
|
||||||
|
; Temporary folder for cache
|
||||||
|
; Used for compiled templates.
|
||||||
|
; Syntax: [DIR]
|
||||||
|
; Default: tmp/
|
||||||
|
TEMP = tmp/
|
||||||
|
|
||||||
|
; Log file folder
|
||||||
|
; Syntax: [DIR]
|
||||||
|
; Default: logs/
|
||||||
|
LOGS = logs/
|
||||||
|
|
||||||
|
; UI folder
|
||||||
|
; Where all the public assets (templates, images, styles, scripts) are located.
|
||||||
|
; Syntax: [DIR]
|
||||||
|
; Default: public/
|
||||||
|
UI = public/
|
||||||
|
|
||||||
|
; Autoload folder
|
||||||
|
; Where PHP attempts to autoload PHP classes at runtime.
|
||||||
|
; Syntax: [DIR]
|
||||||
|
; Default: app/
|
||||||
|
;AUTOLOAD = app/
|
||||||
|
|
||||||
|
; Favicon folder
|
||||||
|
; Syntax: [DIR]
|
||||||
|
; Default: favicon/
|
||||||
|
FAVICON = favicon/
|
||||||
|
|
||||||
|
; Export folder
|
||||||
|
; Where DB dump files are located/created at.
|
||||||
|
; Syntax: [DIR]
|
||||||
|
; Default: export/
|
||||||
|
EXPORT = export/
|
||||||
|
|
||||||
|
; Custom *.ini file folder
|
||||||
|
; Can be used to overwrite default *.ini files and settings
|
||||||
|
; See: https://github.com/exodus4d/pathfinder/wiki/Configuration#custom-confpathfinderini
|
||||||
|
; Syntax: [DIR]
|
||||||
|
CONF.CUSTOM = conf/
|
||||||
|
CONF.DEFAULT = app/
|
||||||
|
|
||||||
|
; Load additional config files
|
||||||
|
; DO NOT load environment.ini, it is loaded automatically
|
||||||
|
[configs]
|
||||||
|
{{@CONF.DEFAULT}}routes.ini = true
|
||||||
|
{{@CONF.DEFAULT}}pathfinder.ini = true
|
||||||
|
{{@CONF.DEFAULT}}plugin.ini = true
|
||||||
|
{{@CONF.CUSTOM}}pathfinder.ini = true
|
||||||
|
{{@CONF.CUSTOM}}plugin.ini = true
|
||||||
|
{{@CONF.DEFAULT}}requirements.ini = true
|
||||||
|
{{@CONF.DEFAULT}}cron.ini = true
|
||||||
397
config/pathfinder/pathfinder.ini
Normal file
397
config/pathfinder/pathfinder.ini
Normal file
|
|
@ -0,0 +1,397 @@
|
||||||
|
; Pathfinder Config
|
||||||
|
|
||||||
|
[PATHFINDER]
|
||||||
|
; Name of installation
|
||||||
|
; This can be changed to any name
|
||||||
|
; This name is used in e.g. emails, user interface
|
||||||
|
; Syntax: String
|
||||||
|
; Default: Pathfinder
|
||||||
|
NAME = Goryn Are Lost
|
||||||
|
|
||||||
|
; Pathfinder version
|
||||||
|
; Version number should not be changed manually.
|
||||||
|
; Version is used for CSS/JS cache busting and is part of the URL for static resources:
|
||||||
|
; e.g. public/js/vX.X.X/app.js
|
||||||
|
; Syntax: String (current version)
|
||||||
|
; Default: v2.0.0
|
||||||
|
VERSION = v2.0.1
|
||||||
|
|
||||||
|
; Contact information [optional]
|
||||||
|
; Shown on 'licence', 'contact' page.
|
||||||
|
; Syntax: String
|
||||||
|
; Default: https://github.com/exodus4d
|
||||||
|
CONTACT = https://github.com/exodus4d
|
||||||
|
|
||||||
|
; Public contact email [optional]
|
||||||
|
; Syntax: String
|
||||||
|
; Default:
|
||||||
|
EMAIL =
|
||||||
|
|
||||||
|
; Repository URL [optional]
|
||||||
|
; Used for 'licence', 'contact' page.
|
||||||
|
; Syntax: String
|
||||||
|
; Default: https://github.com/exodus4d/pathfinder
|
||||||
|
REPO = https://github.com/exodus4d/pathfinder
|
||||||
|
|
||||||
|
; Show warning on 'login' page if /setup route is active
|
||||||
|
; DO NOT disable this warning unless /setup route is protected or commented in routes.ini
|
||||||
|
; Syntax: 0 | 1
|
||||||
|
; Default: 1
|
||||||
|
SHOW_SETUP_WARNING = 0
|
||||||
|
|
||||||
|
; Show full login page
|
||||||
|
; If disabled, some section don´t appear:
|
||||||
|
; 'Slideshow', 'Features', 'Admin', 'Install', 'About'
|
||||||
|
; Syntax: 0 | 1
|
||||||
|
; Default: 1
|
||||||
|
SHOW_COMPLETE_LOGIN_PAGE = 1
|
||||||
|
|
||||||
|
; REGISTRATION ====================================================================================
|
||||||
|
[PATHFINDER.REGISTRATION]
|
||||||
|
; Registration status (for new users)
|
||||||
|
; If disabled, users can no longer register a new account on this installation.
|
||||||
|
; Syntax: 0 | 1
|
||||||
|
; Default: 1
|
||||||
|
STATUS = 1
|
||||||
|
|
||||||
|
[PATHFINDER.LOGIN]
|
||||||
|
; Expire time for login cookies
|
||||||
|
; Login Cookie information send by clients is re-validated by the server.
|
||||||
|
; The expire time for each cookie is stored in DB. Expired Cookies become invalid.
|
||||||
|
; Syntax: Integer (days)
|
||||||
|
; Default: 30
|
||||||
|
COOKIE_EXPIRE = 30
|
||||||
|
|
||||||
|
; Show 'scheduled maintenance' warning
|
||||||
|
; If enabled, active users will see a notification panel.
|
||||||
|
; This can be used to inform users about upcoming maintenance shutdown.
|
||||||
|
; This flag can be enabled "on the fly" (no page reload required to see the notice).
|
||||||
|
; Syntax: 0 | 1
|
||||||
|
; Default: 0
|
||||||
|
MODE_MAINTENANCE = 0
|
||||||
|
|
||||||
|
; Login restrictions (white lists)
|
||||||
|
; Login/registration can be restricted to specific groups.
|
||||||
|
; Use comma separated strings for CCP Ids (e.g. 1000166,1000080).
|
||||||
|
; If no groups are specified, all characters are allowed.
|
||||||
|
; Syntax: String (comma separated)
|
||||||
|
; Default:
|
||||||
|
CHARACTER =
|
||||||
|
CORPORATION =
|
||||||
|
ALLIANCE =
|
||||||
|
|
||||||
|
[PATHFINDER.CHARACTER]
|
||||||
|
; Auto location select for characters
|
||||||
|
; If enabled, characters can activate the "auto location select" checkbox in their account settings.
|
||||||
|
; If checkbox active, solar systems get auto selected on map based on their current system.
|
||||||
|
; Hint: This can increase server load because of more client requests.
|
||||||
|
; Syntax: 0 | 1
|
||||||
|
; Default: 1
|
||||||
|
AUTO_LOCATION_SELECT = 1
|
||||||
|
|
||||||
|
; Slack API integration ===========================================================================
|
||||||
|
[PATHFINDER.SLACK]
|
||||||
|
; Slack API status
|
||||||
|
; This is a global toggle for all Slack related features.
|
||||||
|
; Check PATHFINDER.MAP section for individual control.
|
||||||
|
; Syntax: 0 | 1
|
||||||
|
; Default: 1
|
||||||
|
STATUS = 1
|
||||||
|
|
||||||
|
; Discord API integration =========================================================================
|
||||||
|
[PATHFINDER.DISCORD]
|
||||||
|
; Discord API status
|
||||||
|
; This is a global toggle for all Discord related features.
|
||||||
|
; Check PATHFINDER.MAP section for individual control.
|
||||||
|
; Syntax: 0 | 1
|
||||||
|
; Default: 1
|
||||||
|
STATUS = 1
|
||||||
|
|
||||||
|
; View ============================================================================================
|
||||||
|
[PATHFINDER.VIEW]
|
||||||
|
; Page templates
|
||||||
|
; Hint: You should not change this.
|
||||||
|
INDEX = templates/view/index.html
|
||||||
|
SETUP = templates/view/setup.html
|
||||||
|
LOGIN = templates/view/login.html
|
||||||
|
ADMIN = templates/view/admin.html
|
||||||
|
|
||||||
|
; HTTP status pages ===============================================================================
|
||||||
|
[PATHFINDER.STATUS]
|
||||||
|
; Error page templates
|
||||||
|
; Hint: You should not change this.
|
||||||
|
4XX = templates/status/4xx.html
|
||||||
|
5XX = templates/status/5xx.html
|
||||||
|
|
||||||
|
; MAP =============================================================================================
|
||||||
|
; Map settings for 'private', 'corporation' and 'alliance' maps:
|
||||||
|
; LIFETIME (days)
|
||||||
|
; - Map will be deleted after 'X' days, by cronjob
|
||||||
|
; MAX_COUNT
|
||||||
|
; - Users can create/view up to 'X' maps of a type
|
||||||
|
; MAX_SHARED
|
||||||
|
; - Max number of shared entities per map
|
||||||
|
; MAX_SYSTEMS
|
||||||
|
; - Max number of active systems per map
|
||||||
|
; LOG_ACTIVITY_ENABLED (Syntax: 0 | 1)
|
||||||
|
; - Whether user activity statistics can be enabled for a map type
|
||||||
|
; - E.g. create/update/delete of systems/connections/signatures/...
|
||||||
|
; LOG_HISTORY_ENABLED (Syntax: 0 | 1)
|
||||||
|
; - Whether map change history should be logged to separate *.log files
|
||||||
|
; - see: [PATHFINDER.HISTORY] config section below
|
||||||
|
; SEND_HISTORY_SLACK_ENABLED (Syntax: 0 | 1)
|
||||||
|
; - Send map updates to a Slack channel per map
|
||||||
|
; SEND_RALLY_SLACK_ENABLED (Syntax: 0 | 1)
|
||||||
|
; - Send rally point pokes to a Slack channel per map
|
||||||
|
; SEND_HISTORY_DISCORD_ENABLED (Syntax: 0 | 1)
|
||||||
|
; - Send map updates to a Discord channel per map
|
||||||
|
; SEND_RALLY_DISCORD_ENABLED (Syntax: 0 | 1)
|
||||||
|
; - Send rally point pokes to a Discord channel per map
|
||||||
|
; SEND_RALLY_Mail_ENABLED (Syntax: 0 | 1)
|
||||||
|
; - Send rally point pokes by mail
|
||||||
|
; - see: [PATHFINDER.NOTIFICATION] section below
|
||||||
|
[PATHFINDER.MAP.PRIVATE]
|
||||||
|
LIFETIME = 60
|
||||||
|
MAX_COUNT = 5
|
||||||
|
MAX_SHARED = 10
|
||||||
|
MAX_SYSTEMS = 50
|
||||||
|
LOG_ACTIVITY_ENABLED = 1
|
||||||
|
LOG_HISTORY_ENABLED = 1
|
||||||
|
SEND_HISTORY_SLACK_ENABLED = 0
|
||||||
|
SEND_RALLY_SLACK_ENABLED = 1
|
||||||
|
SEND_HISTORY_DISCORD_ENABLED = 0
|
||||||
|
SEND_RALLY_DISCORD_ENABLED = 1
|
||||||
|
SEND_RALLY_Mail_ENABLED = 0
|
||||||
|
|
||||||
|
[PATHFINDER.MAP.CORPORATION]
|
||||||
|
LIFETIME = 99999
|
||||||
|
MAX_COUNT = 7
|
||||||
|
MAX_SHARED = 4
|
||||||
|
MAX_SYSTEMS = 100
|
||||||
|
LOG_ACTIVITY_ENABLED = 1
|
||||||
|
LOG_HISTORY_ENABLED = 1
|
||||||
|
SEND_HISTORY_SLACK_ENABLED = 1
|
||||||
|
SEND_RALLY_SLACK_ENABLED = 1
|
||||||
|
SEND_HISTORY_DISCORD_ENABLED = 1
|
||||||
|
SEND_RALLY_DISCORD_ENABLED = 1
|
||||||
|
SEND_RALLY_Mail_ENABLED = 0
|
||||||
|
|
||||||
|
[PATHFINDER.MAP.ALLIANCE]
|
||||||
|
LIFETIME = 99999
|
||||||
|
MAX_COUNT = 4
|
||||||
|
MAX_SHARED = 2
|
||||||
|
MAX_SYSTEMS = 100
|
||||||
|
LOG_ACTIVITY_ENABLED = 1
|
||||||
|
LOG_HISTORY_ENABLED = 1
|
||||||
|
SEND_HISTORY_SLACK_ENABLED = 1
|
||||||
|
SEND_RALLY_SLACK_ENABLED = 1
|
||||||
|
SEND_HISTORY_DISCORD_ENABLED = 1
|
||||||
|
SEND_RALLY_DISCORD_ENABLED = 1
|
||||||
|
SEND_RALLY_Mail_ENABLED = 0
|
||||||
|
|
||||||
|
; Route search ====================================================================================
|
||||||
|
[PATHFINDER.ROUTE]
|
||||||
|
; Search depth for system route search
|
||||||
|
; Recursive search depth for search algorithm.
|
||||||
|
; This is only used in case ESIs /route/ API responds with errors and the custom search algorithm is used.
|
||||||
|
; Hint: Higher values can lead to high CPU load. If to low, routes might not be found even if exist.
|
||||||
|
; Syntax: Integer
|
||||||
|
; Default: 9000
|
||||||
|
SEARCH_DEPTH = 9000
|
||||||
|
|
||||||
|
; Initial count of routes that will be checked when a system becomes active
|
||||||
|
; Syntax: Integer
|
||||||
|
; Default: 4
|
||||||
|
SEARCH_DEFAULT_COUNT = 10
|
||||||
|
|
||||||
|
; Max count of routes that can be selected in 'route settings' dialog
|
||||||
|
; Syntax: Integer
|
||||||
|
; Default: 6
|
||||||
|
MAX_DEFAULT_COUNT = 10
|
||||||
|
|
||||||
|
; Max count of routes that will be checked (MAX_DEFAULT_COUNT + custom routes)
|
||||||
|
; Syntax: Integer
|
||||||
|
; Default: 8
|
||||||
|
LIMIT = 12
|
||||||
|
|
||||||
|
; Email notifications =============================================================================
|
||||||
|
[PATHFINDER.NOTIFICATION]
|
||||||
|
; Email address for rally point pokes
|
||||||
|
; Requires SMTP configuration (see environment.ini).
|
||||||
|
; Hint: This only makes sens if the installation is restricted to allied groups only.
|
||||||
|
; This email address is used for all maps on this installation.
|
||||||
|
; Syntax: String
|
||||||
|
; Default:
|
||||||
|
RALLY_SET =
|
||||||
|
|
||||||
|
; TIMER ===========================================================================================
|
||||||
|
; Timer values should NOT be changed unless you know what they affect!
|
||||||
|
; =================================================================================================
|
||||||
|
[PATHFINDER.TIMER]
|
||||||
|
; Login time for characters. Users get logged out after X minutes
|
||||||
|
; Hint: Set to 0 disables login time and characters stay logged in until Cookie data expires
|
||||||
|
; Syntax: Integer (minutes)
|
||||||
|
; Default: 480
|
||||||
|
LOGGED = 480
|
||||||
|
|
||||||
|
; Double click timer
|
||||||
|
; Syntax: Integer (milliseconds)
|
||||||
|
; Default: 250
|
||||||
|
DBL_CLICK = 250
|
||||||
|
|
||||||
|
; Time for status change visibility in header
|
||||||
|
; Syntax: Integer (milliseconds)
|
||||||
|
; Default: 5000
|
||||||
|
PROGRAM_STATUS_VISIBLE = 5000
|
||||||
|
|
||||||
|
[PATHFINDER.TIMER.UPDATE_SERVER_MAP]
|
||||||
|
; Map data update interval (ajax long polling)
|
||||||
|
; This is not used for 'WebSocket' configured installations.
|
||||||
|
; Syntax: Integer (milliseconds)
|
||||||
|
; Default: 5000
|
||||||
|
DELAY = 5000
|
||||||
|
|
||||||
|
; Execution limit for map data update request (ajax long polling)
|
||||||
|
; Requests that exceed the limit are logged as 'warning'.
|
||||||
|
; Syntax: Integer (milliseconds)
|
||||||
|
; Default: 200
|
||||||
|
EXECUTION_LIMIT = 500
|
||||||
|
|
||||||
|
[PATHFINDER.TIMER.UPDATE_CLIENT_MAP]
|
||||||
|
; Execution limit for client side (javascript) map data updates
|
||||||
|
; Map data updates that exceed the limit are logged as 'warning'.
|
||||||
|
; Syntax: Integer (milliseconds)
|
||||||
|
; Default: 50
|
||||||
|
EXECUTION_LIMIT = 100
|
||||||
|
|
||||||
|
[PATHFINDER.TIMER.UPDATE_SERVER_USER_DATA]
|
||||||
|
; User data update interval (ajax long polling)
|
||||||
|
; This is not used for 'WebSocket' configured installations.
|
||||||
|
; Syntax: Integer (milliseconds)
|
||||||
|
; Default: 5000
|
||||||
|
DELAY = 5000
|
||||||
|
|
||||||
|
; Execution limit for user data update request (ajax long polling)
|
||||||
|
; Requests that exceed the limit are logged as 'warning'.
|
||||||
|
; Syntax: Integer (milliseconds)
|
||||||
|
; Default: 500
|
||||||
|
EXECUTION_LIMIT = 1000
|
||||||
|
|
||||||
|
; update client user data (milliseconds)
|
||||||
|
[PATHFINDER.TIMER.UPDATE_CLIENT_USER_DATA]
|
||||||
|
; Execution limit for client side (javascript) user data updates
|
||||||
|
; User data updates that exceed the limit are logged as 'warning'.
|
||||||
|
; Syntax: Integer (milliseconds)
|
||||||
|
; Default: 50
|
||||||
|
EXECUTION_LIMIT = 100
|
||||||
|
|
||||||
|
; CACHE ===========================================================================================
|
||||||
|
[PATHFINDER.CACHE]
|
||||||
|
; Checks "character log" data by cronjob after x seconds
|
||||||
|
; If character is ingame offline -> delete "character log"
|
||||||
|
; Syntax: Integer (seconds)
|
||||||
|
; Default: 180
|
||||||
|
CHARACTER_LOG_INACTIVE = 180
|
||||||
|
|
||||||
|
; Max expire time for cache files
|
||||||
|
; Files will be deleted by cronjob afterwards.
|
||||||
|
; This setting only affects 'file cache'. Redis installations are not affected by this.
|
||||||
|
; Syntax: Integer (seconds)
|
||||||
|
; Default: 864000 (10d)
|
||||||
|
EXPIRE_MAX = 864000
|
||||||
|
|
||||||
|
; Expire time for EOL (end of life) connections
|
||||||
|
; EOL connections get auto deleted by cronjob afterwards.
|
||||||
|
; Syntax: Integer (seconds)
|
||||||
|
; Default: 15300 (4h + 15min)
|
||||||
|
EXPIRE_CONNECTIONS_EOL = 15300
|
||||||
|
|
||||||
|
; Expire time for WH connections
|
||||||
|
; WH connections get auto deleted by cronjob afterwards.
|
||||||
|
; This can be overwritten for each map in the UI.
|
||||||
|
; Syntax: Integer (seconds)
|
||||||
|
; Default: 172800 (2d)
|
||||||
|
EXPIRE_CONNECTIONS_WH = 172800
|
||||||
|
|
||||||
|
; Expire time for signatures (inactive systems)
|
||||||
|
; Signatures get auto deleted by cronjob afterwards.
|
||||||
|
; This can be overwritten for each map in the UI.
|
||||||
|
; Syntax: Integer (seconds)
|
||||||
|
; Default: 259200 (3d)
|
||||||
|
EXPIRE_SIGNATURES = 259200
|
||||||
|
|
||||||
|
; LOGGING =========================================================================================
|
||||||
|
; Log file configurations
|
||||||
|
; Log files are location in [PATHFINDER]/logs/ dir (see: config.ini)
|
||||||
|
; Syntax: String
|
||||||
|
[PATHFINDER.LOGFILES]
|
||||||
|
; Error log
|
||||||
|
ERROR = error
|
||||||
|
; SSO error log
|
||||||
|
SSO = sso
|
||||||
|
; Login info
|
||||||
|
CHARACTER_LOGIN = character_login
|
||||||
|
; Character access
|
||||||
|
CHARACTER_ACCESS = character_access
|
||||||
|
; Session warnings (mysql sessions only)
|
||||||
|
SESSION_SUSPECT = session_suspect
|
||||||
|
; Account deleted
|
||||||
|
DELETE_ACCOUNT = account_delete
|
||||||
|
; Admin action (e.g. kick, ban)
|
||||||
|
ADMIN = admin
|
||||||
|
; TCP socket errors
|
||||||
|
SOCKET_ERROR = socket_error
|
||||||
|
; debug log for development
|
||||||
|
DEBUG = debug
|
||||||
|
|
||||||
|
[PATHFINDER.HISTORY]
|
||||||
|
; cache time for parsed history log file data
|
||||||
|
; Syntax: Integer (seconds)
|
||||||
|
; Default: 5
|
||||||
|
CACHE = 5
|
||||||
|
|
||||||
|
; File folder for 'history' logs (e.g. map history)
|
||||||
|
; Syntax: String
|
||||||
|
; Default: history/
|
||||||
|
LOG = history/
|
||||||
|
|
||||||
|
; Max file size for 'history' logs before getting truncated by cronjob
|
||||||
|
; Syntax: Integer (MB)
|
||||||
|
; Default: 2
|
||||||
|
LOG_SIZE_THRESHOLD = 2
|
||||||
|
|
||||||
|
; log entries (lines) after file getting truncated by cronjob
|
||||||
|
; Syntax: Integer
|
||||||
|
; Default: 1000
|
||||||
|
LOG_LINES = 1000
|
||||||
|
|
||||||
|
; ADMIN ===========================================================================================
|
||||||
|
; "SUPER" admins and additional "CORPORATION" admins can be added here
|
||||||
|
;[PATHFINDER.ROLES]
|
||||||
|
;CHARACTER.0.ID = 123456789
|
||||||
|
;CHARACTER.0.ROLE = SUPER
|
||||||
|
;CHARACTER.1.ID = 1122334455
|
||||||
|
;CHARACTER.1.ROLE = CORPORATION
|
||||||
|
|
||||||
|
; API =============================================================================================
|
||||||
|
[PATHFINDER.API]
|
||||||
|
CCP_IMAGE_SERVER = https://images.evetech.net
|
||||||
|
Z_KILLBOARD = https://zkillboard.com/api
|
||||||
|
EVEEYE = https://eveeye.com
|
||||||
|
DOTLAN = http://evemaps.dotlan.net
|
||||||
|
ANOIK = http://anoik.is
|
||||||
|
EVE_SCOUT = https://www.eve-scout.com/api
|
||||||
|
; GitHub Developer API
|
||||||
|
GIT_HUB = https://api.github.com
|
||||||
|
|
||||||
|
; EXPERIMENTAL [BETA] =============================================================================
|
||||||
|
; Use these settings with caution!
|
||||||
|
; They are currently under testing and might be removed in further releases.
|
||||||
|
[PATHFINDER.EXPERIMENTS]
|
||||||
|
; Try to use persistent database connections
|
||||||
|
; PDO connections get initialized with ATTR_PERSISTENT => true .
|
||||||
|
; http://php.net/manual/en/pdo.connections.php#example-1030
|
||||||
|
; Hint: Set 'wait_timeout' to a high value in your my.conf to keep them open
|
||||||
|
; Syntax: 0 | 1
|
||||||
|
; Default: 0
|
||||||
|
PERSISTENT_DB_CONNECTIONS = 1
|
||||||
93
docker-compose.yml
Normal file
93
docker-compose.yml
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
caddy:
|
||||||
|
image: lucaslorentz/caddy-docker-proxy:ci-alpine
|
||||||
|
container_name: caddy
|
||||||
|
ports:
|
||||||
|
- 80:80
|
||||||
|
- 443:443
|
||||||
|
networks:
|
||||||
|
- caddy
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
# this volume is needed to keep the certificates
|
||||||
|
# otherwise, new ones will be re-issued upon restart
|
||||||
|
- caddy_data:/data
|
||||||
|
labels:
|
||||||
|
caddy.email: carl.egal@gmail.com
|
||||||
|
pfdb:
|
||||||
|
image: bianjp/mariadb-alpine:latest
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: $MYSQL_PASSWORD
|
||||||
|
container_name: "$CONTAINER_NAME-db"
|
||||||
|
networks:
|
||||||
|
pf:
|
||||||
|
aliases:
|
||||||
|
- "${CONTAINER_NAME}db"
|
||||||
|
volumes:
|
||||||
|
- db_data:/var/lib/mysql
|
||||||
|
- $path/pathfinder/export/sql/eve_universe.sql.zip:/eve_universe.sql.zip
|
||||||
|
restart: always
|
||||||
|
pf-redis:
|
||||||
|
image: redis:latest
|
||||||
|
container_name: "$CONTAINER_NAME-redis"
|
||||||
|
command: ["redis-server", "--appendonly", "yes"]
|
||||||
|
hostname: redis
|
||||||
|
volumes:
|
||||||
|
- redis_data:/data
|
||||||
|
networks:
|
||||||
|
pf:
|
||||||
|
aliases:
|
||||||
|
- "$CONTAINER_NAME-redis"
|
||||||
|
logging:
|
||||||
|
driver: none
|
||||||
|
restart: always
|
||||||
|
pf-socket:
|
||||||
|
image: composer:latest
|
||||||
|
container_name: "$CONTAINER_NAME-socket"
|
||||||
|
command: ["sh","-c","composer install && php cmd.php --tcpHost 0.0.0.0"]
|
||||||
|
hostname: socket
|
||||||
|
volumes:
|
||||||
|
- ${path}/websocket:/app
|
||||||
|
networks:
|
||||||
|
pf:
|
||||||
|
aliases:
|
||||||
|
- "$CONTAINER_NAME-socket"
|
||||||
|
restart: always
|
||||||
|
pf:
|
||||||
|
container_name: ${CONTAINER_NAME}
|
||||||
|
hostname: "pathfinder"
|
||||||
|
build: '.'
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
labels:
|
||||||
|
caddy: test.goryn.wtf
|
||||||
|
caddy.reverse_proxy: "{{upstreams 80}}"
|
||||||
|
networks:
|
||||||
|
- pf
|
||||||
|
- caddy
|
||||||
|
# ports:
|
||||||
|
# - 80:80
|
||||||
|
# - 8030:8030
|
||||||
|
healthcheck:
|
||||||
|
disable: true
|
||||||
|
volumes:
|
||||||
|
- ${path}/config/pathfinder/config.ini:/var/www/html/pathfinder/app/templateConfig.ini
|
||||||
|
- ${path}/config/pathfinder/pathfinder.ini:/var/www/html/pathfinder/app/pathfinder.ini
|
||||||
|
- ${path}/config/pathfinder/plugins.ini:/var/www/html/pathfinder/app/plugins.ini
|
||||||
|
depends_on:
|
||||||
|
- pfdb
|
||||||
|
- pf-redis
|
||||||
|
- pf-socket
|
||||||
|
restart: always
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
data:
|
||||||
|
db_data:
|
||||||
|
redis_data:
|
||||||
|
caddy_data: {}
|
||||||
|
networks:
|
||||||
|
pf:
|
||||||
|
caddy:
|
||||||
|
|
||||||
1
pathfinder
Submodule
1
pathfinder
Submodule
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit c3f0bb2eccfd39cb5cd2eee37db3f005e1718630
|
||||||
128
setup.sh
Executable file
128
setup.sh
Executable file
|
|
@ -0,0 +1,128 @@
|
||||||
|
#!/bin/bash
|
||||||
|
. static/menu.sh
|
||||||
|
source $CWD\.env
|
||||||
|
|
||||||
|
|
||||||
|
#https://bytefreaks.net/gnulinux/bash/cecho-a-function-to-print-using-different-colors-in-bash
|
||||||
|
cecho () {
|
||||||
|
declare -A colors;
|
||||||
|
colors=(\
|
||||||
|
['black']='\E[0;47m'\
|
||||||
|
['red']='\E[0;31m'\
|
||||||
|
['green']='\E[0;32m'\
|
||||||
|
['yellow']='\E[0;33m'\
|
||||||
|
['blue']='\E[0;34m'\
|
||||||
|
['magenta']='\E[0;35m'\
|
||||||
|
['cyan']='\E[0;36m'\
|
||||||
|
['white']='\E[0;37m'\
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
local defaultMSG="";
|
||||||
|
local defaultColor="black";
|
||||||
|
local defaultNewLine=true;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
while [[ $# -gt 1 ]];
|
||||||
|
do
|
||||||
|
key="$1";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
case $key in
|
||||||
|
-c|--color)
|
||||||
|
color="$2";
|
||||||
|
shift;
|
||||||
|
;;
|
||||||
|
|
||||||
|
-n|--noline)
|
||||||
|
newLine=false;
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
# unknown option
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
shift;
|
||||||
|
done
|
||||||
|
|
||||||
|
message=${1:-$defaultMSG}; # Defaults to default message.
|
||||||
|
color=${color:-$defaultColor}; # Defaults to default color, if not specified.
|
||||||
|
newLine=${newLine:-$defaultNewLine};
|
||||||
|
|
||||||
|
echo -en "${colors[$color]}";
|
||||||
|
echo -en "$message";
|
||||||
|
if [ "$newLine" = true ] ; then
|
||||||
|
echo;
|
||||||
|
fi
|
||||||
|
tput sgr0; # Reset text attributes to normal without clearing screen.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function wizzard {
|
||||||
|
cecho -c 'blue' "$@";
|
||||||
|
#echo -e "\e[4mMENU: select-one, using assoc keys, preselection, leave selected options\e[24m"
|
||||||
|
#declare -A options2="${$2}"
|
||||||
|
#declare -A options2=( [foo]="Hallo" [bar]="World" [baz]="Record")
|
||||||
|
ui_widget_select -l -k "${!menu[@]}" -s bar -i "${menu[@]}"
|
||||||
|
#echo "Return code: $?"
|
||||||
|
#return "$?"
|
||||||
|
}
|
||||||
|
|
||||||
|
function editVariable(){
|
||||||
|
if [ "$1" == "" ]; then
|
||||||
|
read -p "Please set a config value for $3 [$2]: " VALUE
|
||||||
|
VALUE="${VALUE:-$2}"
|
||||||
|
#sed -i "s/$3=.*/$3=\"$VALUE\"/g" .env
|
||||||
|
sed -i "s@$3=.*@$3=\"$VALUE\"@g" .env
|
||||||
|
#sed -i 's/'"$3"'=.*/'"$3"'='"${VALUE}"'/' .env
|
||||||
|
#sed -i -e 's/'$3'=.*/'$3'="'"$VALUE"'"/g' .env
|
||||||
|
#sed -i "s/$3=.*/$3=$VALUE/g" .env
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
function setConfig(){
|
||||||
|
editVariable "$path" "$PWD" "path"
|
||||||
|
editVariable "$CONTAINER_NAME" "pf" "CONTAINER_NAME"
|
||||||
|
editVariable "$DOMAIN" "localhost" "DOMAIN"
|
||||||
|
editVariable "$SERVER_NAME" "CARLFINDER" "SERVER_NAME"
|
||||||
|
editVariable "$MYSQL_PASSWORD" "" "MYSQL_PASSWORD"
|
||||||
|
editVariable "$CCP_SSO_CLIENT_ID" "" "CCP_SSO_CLIENT_ID"
|
||||||
|
editVariable "$CCP_SSO_SECRET_KEY" "" "CCP_SSO_SECRET_KEY"
|
||||||
|
editVariable "$CCP_ESI_SCOPES" "esi-location.read_online.v1,esi-location.read_location.v1,esi-location.read_ship_type.v1,esi-ui.write_waypoint.v1,esi-ui.open_window.v1,esi-universe.read_structures.v1,esi-corporations.read_corporation_membership.v1,esi-clones.read_clones.v1" "CCP_ESI_SCOPES"
|
||||||
|
source $CWD\.env
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $path == "" ]] || [[ $CONTAINER_NAME == "" ]] || [[ $DOMAIN == "" ]] || [[ $SERVER_NAME == "" ]] || [[ $MYSQL_PASSWORD == "" ]] || [[ $CCP_SSO_CLIENT_ID == "" ]] || [[ $CCP_SSO_SECRET_KEY == "" ]] || [[ $CCP_ESI_SCOPES == "" ]]; do
|
||||||
|
setConfig
|
||||||
|
done
|
||||||
|
|
||||||
|
docker container inspect $CONTAINER_NAME > /dev/null 2>&1;
|
||||||
|
if [ $? -eq 1 ];
|
||||||
|
then
|
||||||
|
declare -A menu=( [no]="NO" [yes]="YES")
|
||||||
|
wizzard "You did not build the container ! Do you want the setup to do it ? "
|
||||||
|
if [ ${menu[$UI_WIDGET_RC]} == "YES" ]; then
|
||||||
|
docker-compose build
|
||||||
|
tput clear;
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
running=$(docker container inspect -f '{{.State.Status}}' $CONTAINER_NAME)
|
||||||
|
if [[ $running != "running" ]];then
|
||||||
|
declare -A menu=( [no]="NO" [yes]="YES")
|
||||||
|
wizzard "Do you want to run the container ?"
|
||||||
|
if [ ${menu[$UI_WIDGET_RC]} == "YES" ]; then
|
||||||
|
docker-compose up -d
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
declare -A menu=( [no]="NO" [yes]="YES ")
|
||||||
|
cecho -c 'blue' "Do you want to import the eve_universe database ?";
|
||||||
|
cecho -c 'red' "DISCLAIMER: Before you do that go to http://$DOMAIN/setup page (USERNAME:'pf' & password is your APP_PASSWORD) and hit create database, setup tables & fix column keys. After you did that select YES."
|
||||||
|
wizzard "";
|
||||||
|
if [ ${menu[$UI_WIDGET_RC]} == "YES" ]; then
|
||||||
|
docker-compose exec pfdb /bin/sh -c "unzip -p eve_universe.sql.zip | mysql -u root -p\$MYSQL_ROOT_PASSWORD eve_universe";
|
||||||
|
fi
|
||||||
|
|
||||||
2
static/crontab.txt
Executable file
2
static/crontab.txt
Executable file
|
|
@ -0,0 +1,2 @@
|
||||||
|
* * * * * cd /var/www/html/pathfinder;sudo -u nobody php index.php /cron >> /var/log/cron.log 2>&1
|
||||||
|
|
||||||
9
static/entrypoint.sh
Normal file
9
static/entrypoint.sh
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
envsubst '$DOMAIN' </etc/nginx/sites_enabled/templateSite.conf >/etc/nginx/sites_enabled/site.conf
|
||||||
|
envsubst '$CONTAINER_NAME' </etc/nginx/templateNginx.conf >/etc/nginx/nginx.conf
|
||||||
|
envsubst </var/www/html/pathfinder/app/templateEnvironment.ini >/var/www/html/pathfinder/app/environment.ini
|
||||||
|
envsubst </var/www/html/pathfinder/app/templateConfig.ini >/var/www/html/pathfinder/app/config.ini
|
||||||
|
envsubst </etc/zzz_custom.ini >/etc/php7/conf.d/zzz_custom.ini
|
||||||
|
htpasswd -c -b -B /etc/nginx/.setup_pass pf "$APP_PASSWORD"
|
||||||
|
exec "$@"
|
||||||
451
static/menu.sh
Normal file
451
static/menu.sh
Normal file
|
|
@ -0,0 +1,451 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
##
|
||||||
|
# Pure BASH interactive CLI/TUI menu (single and multi-select/checkboxes)
|
||||||
|
#
|
||||||
|
# Author: Markus Geiger <mg@evolution515.net>
|
||||||
|
# Last revised 2011-09-11
|
||||||
|
#
|
||||||
|
# ATTENTION! TO BE REFACTORED! FIRST DRAFT!
|
||||||
|
#
|
||||||
|
# Demo
|
||||||
|
#
|
||||||
|
# - ASCIINEMA
|
||||||
|
# https://asciinema.org/a/Y4hLxnN20JtAlrn3hsC6dCRn8
|
||||||
|
#
|
||||||
|
# Inspired by
|
||||||
|
#
|
||||||
|
# - https://serverfault.com/questions/144939/multi-select-menu-in-bash-script
|
||||||
|
# - Copyright (C) 2017 Ingo Hollmann - All Rights Reserved
|
||||||
|
# https://www.bughunter2k.de/blog/cursor-controlled-selectmenu-in-bash
|
||||||
|
#
|
||||||
|
# Notes
|
||||||
|
#
|
||||||
|
# - This is a hacky first implementation for my shell tools/dotfiles (ZSH)
|
||||||
|
# - Intention is to use it for CLI wizards (my aim is NOT a full blown curses TUI window interface)
|
||||||
|
# - I concerted TPUT to ANSII-sequences to spare command executions (e.g. `tput ed | xxd`)
|
||||||
|
# reference: http://linuxcommand.org/lc3_adv_tput.php
|
||||||
|
#
|
||||||
|
# Permission to copy and modify is granted under the Creative Commons Attribution 4.0 license
|
||||||
|
#
|
||||||
|
|
||||||
|
# Strict bash scripting (not yet)
|
||||||
|
# set -euo pipefail -o errtrace
|
||||||
|
|
||||||
|
|
||||||
|
# Templates for ui_widget_select
|
||||||
|
declare -xr UI_WIDGET_SELECT_TPL_SELECTED='\e[33m → %s \e[39m'
|
||||||
|
declare -xr UI_WIDGET_SELECT_TPL_DEFAULT=" \e[37m%s %s\e[39m"
|
||||||
|
declare -xr UI_WIDGET_MULTISELECT_TPL_SELECTED="\e[33m → %s %s\e[39m"
|
||||||
|
declare -xr UI_WIDGET_MULTISELECT_TPL_DEFAULT=" \e[37m%s %s\e[39m"
|
||||||
|
declare -xr UI_WIDGET_TPL_CHECKED="▣"
|
||||||
|
declare -xr UI_WIDGET_TPL_UNCHECKED="□"
|
||||||
|
|
||||||
|
# We use env variable to pass results since no interactive output from subshells and we don't wanna go hacky!
|
||||||
|
declare -xg UI_WIDGET_RC=-1
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Get type of a BASH variable (BASH ≥v4.0)
|
||||||
|
#
|
||||||
|
# Notes
|
||||||
|
# - if references are encountered it will automatically try
|
||||||
|
# to resolve them unless '-f' is passed!
|
||||||
|
# - resolving functions can be seen as bonus since they also
|
||||||
|
# use `declare` (but with -fF). this behavior should be removed!
|
||||||
|
# - bad indicates bad referencing which normally shouldn't occur!
|
||||||
|
# - types are shorthand and associative arrays map to "map" for convenience
|
||||||
|
#
|
||||||
|
# argument
|
||||||
|
# -f (optional) force resolvement of first hit
|
||||||
|
# <variable-name> Variable name
|
||||||
|
#
|
||||||
|
# stdout
|
||||||
|
# (nil|number|array|map|reference)
|
||||||
|
#
|
||||||
|
# stderr
|
||||||
|
# -
|
||||||
|
#
|
||||||
|
# return
|
||||||
|
# 0 - always
|
||||||
|
typeof() {
|
||||||
|
# __ref: avoid local to overwrite global var declaration and therefore emmit wrong results!
|
||||||
|
local type="" resolve_ref=true __ref="" signature=()
|
||||||
|
if [[ "$1" == "-f" ]]; then
|
||||||
|
# do not resolve reference
|
||||||
|
resolve_ref=false; shift;
|
||||||
|
fi
|
||||||
|
__ref="$1"
|
||||||
|
while [[ -z "${type}" ]] || ( ${resolve_ref} && [[ "${type}" == *n* ]] ); do
|
||||||
|
IFS=$'\x20\x0a\x3d\x22' && signature=($(declare -p "$__ref" 2>/dev/null || echo "na"))
|
||||||
|
if [[ ! "${signature}" == "na" ]]; then
|
||||||
|
type="${signature[1]}" # could be -xn!
|
||||||
|
fi
|
||||||
|
if [[ -z "${__ref}" ]] || [[ "${type}" == "na" ]] || [[ "${type}" == "" ]]; then
|
||||||
|
printf "nil"
|
||||||
|
return 0
|
||||||
|
elif [[ "${type}" == *n* ]]; then
|
||||||
|
__ref="${signature[4]}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
case "$type" in
|
||||||
|
*i*) printf "number";;
|
||||||
|
*a*) printf "array";;
|
||||||
|
*A*) printf "map";;
|
||||||
|
*n*) printf "reference";;
|
||||||
|
*) printf "string";;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# Removes a value from an array
|
||||||
|
#
|
||||||
|
# alternatives
|
||||||
|
# array=( "${array[@]/$delete}"
|
||||||
|
#
|
||||||
|
# arguments
|
||||||
|
# arg1 value
|
||||||
|
# arg* list or stdin
|
||||||
|
#
|
||||||
|
# stdout
|
||||||
|
# list with space seperator
|
||||||
|
array_without_value() {
|
||||||
|
local args=() value="${1}" s
|
||||||
|
shift
|
||||||
|
for s in "${@}"; do
|
||||||
|
if [ "${value}" != "${s}" ]; then
|
||||||
|
args+=("${s}")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo "${args[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
##
|
||||||
|
# check if a value is in an array
|
||||||
|
#
|
||||||
|
# alternatives
|
||||||
|
# array=( "${array[@]/$delete}"
|
||||||
|
#
|
||||||
|
# arguments
|
||||||
|
# arg1 value
|
||||||
|
# arg* list or stdin
|
||||||
|
#
|
||||||
|
# stdout
|
||||||
|
# list with space seperator
|
||||||
|
array_contains_value() {
|
||||||
|
local e match="$1"
|
||||||
|
shift
|
||||||
|
for e; do [[ "$e" == "$match" ]] && return 0; done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
##
|
||||||
|
# BASH only string to hex
|
||||||
|
#
|
||||||
|
# stdout
|
||||||
|
# hex squence
|
||||||
|
str2hex_echo() {
|
||||||
|
# USAGE: hex_repr=$(str2hex_echo "ABC")
|
||||||
|
# returns "0x410x420x43"
|
||||||
|
local str=${1:-$(cat -)}
|
||||||
|
local fmt=""
|
||||||
|
local chr
|
||||||
|
local -i i
|
||||||
|
printf "0x"
|
||||||
|
for i in `seq 0 $((${#str}-1))`; do
|
||||||
|
chr=${str:i:1}
|
||||||
|
printf "%x" "'${chr}"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
##
|
||||||
|
# Read key and map to human readable output
|
||||||
|
#
|
||||||
|
# notes
|
||||||
|
# output prefix (concated by `-`)
|
||||||
|
# c ctrl key
|
||||||
|
# a alt key
|
||||||
|
# c-a ctrl+alt key
|
||||||
|
# use F if you mean shift!
|
||||||
|
# uppercase `f` for `c+a` combination is not possible!
|
||||||
|
#
|
||||||
|
# arguments
|
||||||
|
# -d for debugging keycodes (hex output via xxd)
|
||||||
|
# -l lowercase all chars
|
||||||
|
# -l <timeout> timeout
|
||||||
|
#
|
||||||
|
# stdout
|
||||||
|
# mapped key code like in notes
|
||||||
|
ui_key_input() {
|
||||||
|
local key
|
||||||
|
local ord
|
||||||
|
local debug=0
|
||||||
|
local lowercase=0
|
||||||
|
local prefix=''
|
||||||
|
local args=()
|
||||||
|
local opt
|
||||||
|
|
||||||
|
while (( "$#" )); do
|
||||||
|
opt="${1}"
|
||||||
|
shift
|
||||||
|
case "${opt}" in
|
||||||
|
"-d") debug=1;;
|
||||||
|
"-l") lowercase=1;;
|
||||||
|
"-t") args+=(-t $1); shift;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
IFS= read ${args[@]} -rsn1 key 2>/dev/null >&2
|
||||||
|
read -sN1 -t 0.0001 k1; read -sN1 -t 0.0001 k2; read -sN1 -t 0.0001 k3
|
||||||
|
key+="${k1}${k2}${k3}"
|
||||||
|
if [[ "${debug}" -eq 1 ]]; then echo -n "${key}" | str2hex_echo; echo -n " : " ;fi;
|
||||||
|
case "${key}" in
|
||||||
|
'') key=enter;;
|
||||||
|
' ') key=space;;
|
||||||
|
$'\x1b') key=esc;;
|
||||||
|
$'\x1b\x5b\x36\x7e') key=pgdown;;
|
||||||
|
$'\x1b\x5b\x33\x7e') key=erase;;
|
||||||
|
$'\x7f') key=backspace;;
|
||||||
|
$'\e[A'|$'\e0A '|$'\e[D'|$'\e0D') key=up;;
|
||||||
|
$'\e[B'|$'\e0B'|$'\e[C'|$'\e0C') key=down;;
|
||||||
|
$'\e[1~'|$'\e0H'|$'\e[H') key=home;;
|
||||||
|
$'\e[4~'|$'\e0F'|$'\e[F') key=end;;
|
||||||
|
$'\e') key=enter;;
|
||||||
|
$'\e'?) prefix="a-"; key="${key:1:1}";;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# only lowercase if we have a single letter
|
||||||
|
# ctrl key is hidden within char code (no o)
|
||||||
|
if [[ "${#key}" == 1 ]]; then
|
||||||
|
ord=$(LC_CTYPE=C printf '%d' "'${key}")
|
||||||
|
if [[ "${ord}" -lt 32 ]]; then
|
||||||
|
prefix="c-${prefix}"
|
||||||
|
# ord=$(([##16] ord + 0x60))
|
||||||
|
# let "ord = [##16] ${ord} + 0x60"
|
||||||
|
ord="$(printf "%X" $((ord + 0x60)))"
|
||||||
|
key="$(printf "\x${ord}")"
|
||||||
|
fi
|
||||||
|
if [[ "${lowercase}" -eq 1 ]]; then
|
||||||
|
key="${key,,}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "${prefix}${key}"
|
||||||
|
}
|
||||||
|
|
||||||
|
##
|
||||||
|
# UI Widget Select
|
||||||
|
#
|
||||||
|
# arguments
|
||||||
|
# -i <[menu-item(s)] …> menu items
|
||||||
|
# -m activate multi-select mode (checkboxes)
|
||||||
|
# -k <[key(s)] …> keys for menu items (if none given indexes are used)
|
||||||
|
# -s <[selected-keys(s)] …> selected keys (index or key)
|
||||||
|
# if keys are used selection needs to be keys
|
||||||
|
# -c clear complete menu on exit
|
||||||
|
# -l clear menu and leave selections
|
||||||
|
#
|
||||||
|
# env
|
||||||
|
# UI_WIDGET_RC will be selected index or -1 of nothing was selected
|
||||||
|
#
|
||||||
|
# stdout
|
||||||
|
# menu display - don't use subshell since we need interactive shell and use tput!
|
||||||
|
#
|
||||||
|
# stderr
|
||||||
|
# sometimes (trying to clean up)
|
||||||
|
#
|
||||||
|
# return
|
||||||
|
# 0 success
|
||||||
|
# -1 cancelled
|
||||||
|
ui_widget_select() {
|
||||||
|
local menu=() keys=() selection=() selection_index=()
|
||||||
|
local cur=0 oldcur=0 collect="item" select="one"
|
||||||
|
local sel="" marg="" drawn=false ref v=""
|
||||||
|
local opt_clearonexit=false opt_leaveonexit=false
|
||||||
|
export UI_WIDGET_RC=-1
|
||||||
|
while (( "$#" )); do
|
||||||
|
opt="${1}"; shift
|
||||||
|
case "${opt}" in
|
||||||
|
-k) collect="key";;
|
||||||
|
-i) collect="item";;
|
||||||
|
-s) collect="selection";;
|
||||||
|
-m) select="multi";;
|
||||||
|
-l) opt_clearonexit=true; opt_leaveonexit=true;;
|
||||||
|
-c) opt_clearonexit=true;;
|
||||||
|
*)
|
||||||
|
if [[ "${collect}" == "selection" ]]; then
|
||||||
|
selection+=("${opt}")
|
||||||
|
elif [[ "${collect}" == "key" ]]; then
|
||||||
|
keys+=("${opt}")
|
||||||
|
else
|
||||||
|
menu+=("$opt")
|
||||||
|
fi;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# sanity check
|
||||||
|
if [[ "${#menu[@]}" -eq 0 ]]; then
|
||||||
|
>&2 echo "no menu items given"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${#keys[@]}" -gt 0 ]]; then
|
||||||
|
# if keys are used
|
||||||
|
# sanity check
|
||||||
|
if [[ "${#keys[@]}" -gt 0 ]] && [[ "${#keys[@]}" != "${#menu[@]}" ]]; then
|
||||||
|
>&2 echo "number of keys do not match menu options!"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
# map keys to indexes
|
||||||
|
selection_index=()
|
||||||
|
for sel in "${selection[@]}"; do
|
||||||
|
for ((i=0;i<${#keys[@]};i++)); do
|
||||||
|
if [[ "${keys[i]}" == "${sel}" ]]; then
|
||||||
|
selection_index+=("$i")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
done
|
||||||
|
else
|
||||||
|
# if no keys are used assign by indexes
|
||||||
|
selection_index=(${selection[@]})
|
||||||
|
fi
|
||||||
|
|
||||||
|
clear_menu() {
|
||||||
|
local str=""
|
||||||
|
for i in "${menu[@]}"; do str+="\e[2K\r\e[1A"; done
|
||||||
|
echo -en "${str}"
|
||||||
|
}
|
||||||
|
|
||||||
|
##
|
||||||
|
# draws menu in three different states
|
||||||
|
# - initial: draw every line as intenden
|
||||||
|
# - update: only draw updated lines and skip existing
|
||||||
|
# - exit: only draw selected lines
|
||||||
|
draw_menu() {
|
||||||
|
local mode="${initial:-$1}" check=false check_tpl="" str="" msg="" tpl_selected="" tpl_default="" marg=()
|
||||||
|
|
||||||
|
if ${drawn} && [[ "$mode" != "exit" ]]; then
|
||||||
|
# reset position
|
||||||
|
str+="\r\e[2K"
|
||||||
|
for i in "${menu[@]}"; do str+="\e[1A"; done
|
||||||
|
# str+="${TPUT_ED}"
|
||||||
|
fi
|
||||||
|
if [[ "$select" == "one" ]]; then
|
||||||
|
tpl_selected="$UI_WIDGET_SELECT_TPL_SELECTED"
|
||||||
|
tpl_default="$UI_WIDGET_SELECT_TPL_DEFAULT"
|
||||||
|
else
|
||||||
|
tpl_selected="$UI_WIDGET_MULTISELECT_TPL_SELECTED"
|
||||||
|
tpl_default="$UI_WIDGET_MULTISELECT_TPL_DEFAULT"
|
||||||
|
fi
|
||||||
|
|
||||||
|
for ((i=0;i<${#menu[@]};i++)); do
|
||||||
|
check=false
|
||||||
|
if [[ "$select" == "one" ]]; then
|
||||||
|
# single selection
|
||||||
|
marg=("${menu[${i}]}")
|
||||||
|
if [[ ${cur} == ${i} ]]; then
|
||||||
|
check=true
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# multi-select
|
||||||
|
check_tpl="$UI_WIDGET_TPL_UNCHECKED";
|
||||||
|
if array_contains_value "$i" "${selection_index[@]}"; then
|
||||||
|
check_tpl="$UI_WIDGET_TPL_CHECKED"; check=true
|
||||||
|
fi
|
||||||
|
marg=("${check_tpl}" "${menu[${i}]}")
|
||||||
|
fi
|
||||||
|
if [[ "${mode}" != "exit" ]] && [[ ${cur} == ${i} ]]; then
|
||||||
|
str+="$(printf "\e[2K${tpl_selected}" "${marg[@]}")\n";
|
||||||
|
elif ([[ "${mode}" != "exit" ]] && ([[ "${oldcur}" == "${i}" ]] || [[ "${mode}" == "initial" ]])) || (${check} && [[ "${mode}" == "exit" ]]); then
|
||||||
|
str+="$(printf "\e[2K${tpl_default}" "${marg[@]}")\n";
|
||||||
|
elif [[ "${mode}" -eq "update" ]] && [[ "${mode}" != "exit" ]]; then
|
||||||
|
str+="\e[1B\r"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
echo -en "${str}"
|
||||||
|
export drawn=true
|
||||||
|
}
|
||||||
|
|
||||||
|
# initial draw
|
||||||
|
draw_menu initial
|
||||||
|
|
||||||
|
# action loop
|
||||||
|
while true; do
|
||||||
|
oldcur=${cur}
|
||||||
|
key=$(ui_key_input)
|
||||||
|
case "${key}" in
|
||||||
|
up|left|i|j) ((cur > 0)) && ((cur--));;
|
||||||
|
down|right|k|l) ((cur < ${#menu[@]}-1)) && ((cur++));;
|
||||||
|
home) cur=0;;
|
||||||
|
pgup) let cur-=5; if [[ "${cur}" -lt 0 ]]; then cur=0; fi;;
|
||||||
|
pgdown) let cur+=5; if [[ "${cur}" -gt $((${#menu[@]}-1)) ]]; then cur=$((${#menu[@]}-1)); fi;;
|
||||||
|
end) ((cur=${#menu[@]}-1));;
|
||||||
|
space)
|
||||||
|
if [[ "$select" == "one" ]]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if ! array_contains_value "$cur" "${selection_index[@]}"; then
|
||||||
|
selection_index+=("$cur")
|
||||||
|
else
|
||||||
|
selection_index=($(array_without_value "$cur" "${selection_index[@]}"))
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
enter)
|
||||||
|
if [[ "${select}" == "multi" ]]; then
|
||||||
|
export UI_WIDGET_RC=()
|
||||||
|
for i in ${selection_index[@]}; do
|
||||||
|
if [[ "${#keys[@]}" -gt 0 ]]; then
|
||||||
|
export UI_WIDGET_RC+=("${keys[${i}]}")
|
||||||
|
else
|
||||||
|
export UI_WIDGET_RC+=("${i}")
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
else
|
||||||
|
if [[ "${#keys[@]}" -gt 0 ]]; then
|
||||||
|
export UI_WIDGET_RC="${keys[${cur}]}";
|
||||||
|
else
|
||||||
|
export UI_WIDGET_RC=${cur};
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if $opt_clearonexit; then clear_menu; fi
|
||||||
|
if $opt_leaveonexit; then draw_menu exit; fi
|
||||||
|
return
|
||||||
|
;;
|
||||||
|
[1-9])
|
||||||
|
let "cur = ${key}"
|
||||||
|
if [[ ${#menu[@]} -gt 9 ]]; then
|
||||||
|
echo -n "${key}"
|
||||||
|
sleep 1
|
||||||
|
key="$(ui_key_input -t 0.5 )"
|
||||||
|
if [[ "$key" =~ [0-9] ]]; then
|
||||||
|
let "cur = cur * 10 + ${key}"
|
||||||
|
elif [[ "$key" != "enter" ]]; then
|
||||||
|
echo -en "\e[2K\r$key invalid input!"
|
||||||
|
sleep 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
let "cur = cur - 1"
|
||||||
|
if [[ ${cur} -gt ${#menu[@]}-1 ]]; then
|
||||||
|
echo -en "\e[2K\rinvalid index!"
|
||||||
|
sleep 1
|
||||||
|
cur="${oldcur}"
|
||||||
|
fi
|
||||||
|
echo -en "\e[2K\r"
|
||||||
|
;;
|
||||||
|
esc|q|$'\e')
|
||||||
|
if $opt_clearonexit; then clear_menu; fi
|
||||||
|
return 1;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Redraw menu
|
||||||
|
draw_menu update
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
247
static/nginx/nginx.conf
Executable file
247
static/nginx/nginx.conf
Executable file
|
|
@ -0,0 +1,247 @@
|
||||||
|
# nginx Configuration File
|
||||||
|
# http://wiki.nginx.org/Configuration
|
||||||
|
|
||||||
|
# Run as a less privileged user for security reasons.
|
||||||
|
user nobody nobody;
|
||||||
|
|
||||||
|
# How many worker threads to run;
|
||||||
|
# "auto" sets it to the number of CPU cores available in the system, and
|
||||||
|
# offers the best performance. Don't set it higher than the number of CPU
|
||||||
|
# cores if changing this parameter.
|
||||||
|
|
||||||
|
# The maximum number of connections for Nginx is calculated by:
|
||||||
|
# max_clients = worker_processes * worker_connections
|
||||||
|
# (2 Cores = 4 processes) check cores: grep processor /proc/cpuinfo | wc -l
|
||||||
|
#worker_processes auto;
|
||||||
|
worker_processes 4;
|
||||||
|
|
||||||
|
# Maximum open file descriptors per process;
|
||||||
|
# should be > worker_connections.
|
||||||
|
worker_rlimit_nofile 20000;
|
||||||
|
|
||||||
|
events {
|
||||||
|
# The worker_connections command tells our worker processes how many people can simultaneously be served by Nginx.
|
||||||
|
# When you need > 8000 * cpu_cores connections, you start optimizing your OS,
|
||||||
|
# and this is probably the point at which you hire people who are smarter than
|
||||||
|
# you, as this is *a lot* of requests.
|
||||||
|
# worker_connections 768;
|
||||||
|
worker_connections 19000;
|
||||||
|
multi_accept on;
|
||||||
|
use epoll;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Default error log file
|
||||||
|
# (this is only used when you don't override error_log on a server{} level)
|
||||||
|
error_log /var/lib/nginx/error.log warn;
|
||||||
|
pid /var/run/nginx.pid;
|
||||||
|
|
||||||
|
|
||||||
|
http {
|
||||||
|
|
||||||
|
# Hide nginx version information.
|
||||||
|
server_tokens on;
|
||||||
|
|
||||||
|
# Define the MIME types for files.
|
||||||
|
include /etc/nginx/mime.types;
|
||||||
|
default_type application/octet-stream;
|
||||||
|
|
||||||
|
# Update charset_types due to updated mime.types
|
||||||
|
charset_types text/css text/plain text/vnd.wap.wml application/javascript application/json application/rss+xml application/xml;
|
||||||
|
|
||||||
|
# Speed up file transfers by using sendfile() to copy directly
|
||||||
|
# between descriptors rather than using read()/write().
|
||||||
|
# For performance reasons, on FreeBSD systems w/ ZFS
|
||||||
|
# this option should be disabled as ZFS's ARC caches
|
||||||
|
# frequently used files in RAM by default.
|
||||||
|
sendfile on;
|
||||||
|
|
||||||
|
# Tell Nginx not to send out partial frames; this increases throughput
|
||||||
|
# since TCP frames are filled up before being sent out. (adds TCP_CORK)
|
||||||
|
tcp_nopush off;
|
||||||
|
|
||||||
|
# Send packages immediately (on). Otherwise nginx will "wait" 200ms for additional data to fullfill a package.
|
||||||
|
tcp_nodelay on;
|
||||||
|
|
||||||
|
# Timeouts ==================================================================================================================
|
||||||
|
|
||||||
|
# 'Body' and 'Header' max response timings. If neither a body or header is sent, the server will issue a 408 error or Request time out. (Default: 60s)
|
||||||
|
client_body_timeout 12;
|
||||||
|
client_header_timeout 12;
|
||||||
|
|
||||||
|
# Assigns the timeout for keep-alive connections with the client.
|
||||||
|
# Simply put, Nginx will close connections with the client after this period of time.(Default: 65)
|
||||||
|
keepalive_timeout 20s;
|
||||||
|
|
||||||
|
# Finally, the send_timeout is established not on the entire transfer of answer, but only between two operations of reading;
|
||||||
|
# if after this time client will take nothing, then Nginx is shutting down the connection.
|
||||||
|
send_timeout 10s;
|
||||||
|
|
||||||
|
# Sets a timeout for name resolution. (Default: 30s)
|
||||||
|
resolver_timeout 5s;
|
||||||
|
|
||||||
|
# Timeout period for connection with FastCGI-server. It should be noted that this value can't exceed 75 seconds. (Default: 60s)
|
||||||
|
fastcgi_connect_timeout 5s;
|
||||||
|
|
||||||
|
# Amount of time for upstream to wait for a fastcgi process to send data.
|
||||||
|
# Change this directive if you have long running fastcgi processes that do not produce output until they have finished processing.
|
||||||
|
# If you are seeing an upstream timed out error in the error log, then increase this parameter to something more appropriate. (Default: 60s)
|
||||||
|
fastcgi_read_timeout 40s;
|
||||||
|
|
||||||
|
# Request timeout to the server. The timeout is calculated between two write operations, not for the whole request.
|
||||||
|
# If no data have been written during this period then serve closes the connection. (Default: 60s)
|
||||||
|
fastcgi_send_timeout 15s;
|
||||||
|
|
||||||
|
# WebSockets ===============================================================================================================
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Buffer ====================================================================================================================
|
||||||
|
|
||||||
|
# Similar to the previous directive, only instead it handles the client header size.
|
||||||
|
# For all intents and purposes, 1K is usually a decent size for this directive.
|
||||||
|
client_header_buffer_size 1k;
|
||||||
|
|
||||||
|
# The maximum number and size of buffers for large client headers.
|
||||||
|
large_client_header_buffers 4 4k;
|
||||||
|
|
||||||
|
# The maximum allowed size for a client request. If the maximum size is exceeded, then Nginx will spit out a 413 error or Request Entity Too Large. (Default: 1m)
|
||||||
|
# php max upload limit cannot be larger than this
|
||||||
|
client_max_body_size 8m;
|
||||||
|
|
||||||
|
# This handles the client buffer size, meaning any POST actions sent to Nginx. POST actions are typically form submissions.
|
||||||
|
client_body_buffer_size 32k;
|
||||||
|
|
||||||
|
output_buffers 2 32k;
|
||||||
|
|
||||||
|
fastcgi_buffering on;
|
||||||
|
fastcgi_buffers 8 32k;
|
||||||
|
fastcgi_buffer_size 32k;
|
||||||
|
|
||||||
|
# Caching ==================================================================================================================
|
||||||
|
|
||||||
|
# Above sample tells nginx to cache a file information as long as minimum 2 requests are made during 5m window.
|
||||||
|
open_file_cache max=10000 inactive=5m;
|
||||||
|
open_file_cache_valid 2m;
|
||||||
|
open_file_cache_min_uses 1;
|
||||||
|
open_file_cache_errors on;
|
||||||
|
|
||||||
|
# Fast CGI
|
||||||
|
# fastcgi_cache_path /etc/nginx/cache levels=1:2 keys_zone=MYAPP:100m inactive=60m;
|
||||||
|
# fastcgi_cache_key "$scheme$request_method$host$request_uri";
|
||||||
|
|
||||||
|
# Logging ===================================================================================================================
|
||||||
|
|
||||||
|
# Format to use in log files
|
||||||
|
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||||
|
'$status $body_bytes_sent "$http_referer" '
|
||||||
|
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||||
|
|
||||||
|
# Extended Logging (e.g. for Nginx Aplify log graphs)
|
||||||
|
log_format main_ext '$remote_addr - $remote_user [$time_local] "$request" '
|
||||||
|
'$status $body_bytes_sent "$http_referer" '
|
||||||
|
'"$http_user_agent" "$http_x_forwarded_for" '
|
||||||
|
'"$host" sn="$server_name" '
|
||||||
|
'rt=$request_time '
|
||||||
|
'ua="$upstream_addr" us="$upstream_status" '
|
||||||
|
'ut="$upstream_response_time" ul="$upstream_response_length" '
|
||||||
|
'cs=$upstream_cache_status' ;
|
||||||
|
|
||||||
|
# This excludes 2xx and 3xx status codes from beeing loged
|
||||||
|
map $status $loggable {
|
||||||
|
~^[23] 0;
|
||||||
|
default 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
# logs just 5xxx errors
|
||||||
|
map $status $log_production {
|
||||||
|
~^[1234] 0;
|
||||||
|
default 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
map $http_upgrade $connection_upgrade {
|
||||||
|
default upgrade;
|
||||||
|
'' close;
|
||||||
|
}
|
||||||
|
|
||||||
|
upstream websocket {
|
||||||
|
server ${CONTAINER_NAME}-socket:8020;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Default log file
|
||||||
|
# (this is only used when you don't override access_log on a server{} level)
|
||||||
|
access_log /var/log/nginx/access.log main if=$loggable;
|
||||||
|
|
||||||
|
# Compression ===============================================================================================================
|
||||||
|
|
||||||
|
# Enable Gzip compressed.
|
||||||
|
gzip on;
|
||||||
|
|
||||||
|
# Compression level (1-9).
|
||||||
|
# 5 is a perfect compromise between size and cpu usage, offering about
|
||||||
|
# 75% reduction for most ascii files (almost identical to level 9).
|
||||||
|
gzip_comp_level 5;
|
||||||
|
|
||||||
|
# Don't compress anything that's already small and unlikely to shrink much
|
||||||
|
# if at all (the default is 20 bytes, which is bad as that usually leads to
|
||||||
|
# larger files after gzipping).
|
||||||
|
gzip_min_length 256;
|
||||||
|
|
||||||
|
# Compress data even for clients that are connecting to us via proxies,
|
||||||
|
# identified by the "Via" header (required for CloudFront).
|
||||||
|
# gzip_proxied expired no-cache no-store private auth;
|
||||||
|
gzip_proxied any;
|
||||||
|
|
||||||
|
# Tell proxies to cache both the gzipped and regular version of a resource
|
||||||
|
# whenever the client's Accept-Encoding capabilities header varies;
|
||||||
|
# Avoids the issue where a non-gzip capable client (which is extremely rare
|
||||||
|
# today) would display gibberish if their proxy gave them the gzipped version.
|
||||||
|
gzip_vary on;
|
||||||
|
|
||||||
|
# Compress all output labeled with one of the following MIME-types.
|
||||||
|
gzip_types
|
||||||
|
application/atom+xml
|
||||||
|
application/javascript
|
||||||
|
application/json
|
||||||
|
application/ld+json
|
||||||
|
application/manifest+json
|
||||||
|
application/rss+xml
|
||||||
|
application/vnd.geo+json
|
||||||
|
application/vnd.ms-fontobject
|
||||||
|
application/x-font-ttf
|
||||||
|
application/x-web-app-manifest+json
|
||||||
|
application/xhtml+xml
|
||||||
|
application/xml
|
||||||
|
font/opentype
|
||||||
|
image/bmp
|
||||||
|
image/svg+xml
|
||||||
|
image/x-icon
|
||||||
|
text/cache-manifest
|
||||||
|
text/css
|
||||||
|
text/plain
|
||||||
|
text/vcard
|
||||||
|
text/vnd.rim.location.xloc
|
||||||
|
text/vtt
|
||||||
|
text/x-component
|
||||||
|
text/x-cross-domain-policy;
|
||||||
|
# text/html;
|
||||||
|
|
||||||
|
# This should be turned on if you are going to have pre-compressed copies (.gz) of
|
||||||
|
# static files available. If not it should be left off as it will cause extra I/O
|
||||||
|
# for the check. It is best if you enable this in a location{} block for
|
||||||
|
# a specific directory, or on an individual server{} level.
|
||||||
|
gzip_static off;
|
||||||
|
|
||||||
|
proxy_buffers 16 16k;
|
||||||
|
proxy_buffer_size 16k;
|
||||||
|
|
||||||
|
# Include files in the sites-enabled folder. server{} configuration files should be
|
||||||
|
# placed in the sites-available folder, and then the configuration should be enabled
|
||||||
|
# by creating a symlink to it in the sites-enabled folder.
|
||||||
|
# See doc/sites-enabled.md for more info.
|
||||||
|
# include /etc/nginx/conf.d/*.conf;
|
||||||
|
include /etc/nginx/sites_enabled/*.conf;
|
||||||
|
}
|
||||||
89
static/nginx/site.conf
Executable file
89
static/nginx/site.conf
Executable file
|
|
@ -0,0 +1,89 @@
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
listen [::]:80;
|
||||||
|
#listen [::]:80 default_server ipv6only=on;
|
||||||
|
server_name $DOMAIN;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Path to static files
|
||||||
|
root /var/www/html/pathfinder/;
|
||||||
|
index index.php index.html index.htm;
|
||||||
|
|
||||||
|
# Specify a charset
|
||||||
|
charset utf-8;
|
||||||
|
|
||||||
|
|
||||||
|
# Logging ===================================================================================================================
|
||||||
|
|
||||||
|
|
||||||
|
location = /setup {
|
||||||
|
auth_basic "Setup Login";
|
||||||
|
auth_basic_user_file /etc/nginx/.setup_pass;
|
||||||
|
try_files $uri $uri/ /index.php?$query_string;
|
||||||
|
}
|
||||||
|
|
||||||
|
location / {
|
||||||
|
# First attempt to serve request as file, then
|
||||||
|
# as directory, then fall back to index.php
|
||||||
|
try_files $uri $uri/ /index.php?q=$uri&$args;
|
||||||
|
}
|
||||||
|
|
||||||
|
# redirect server error pages to the static page /50x.html
|
||||||
|
#
|
||||||
|
error_page 500 502 503 504 /50x.html;
|
||||||
|
location = /50x.html {
|
||||||
|
root /var/lib/nginx/html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
|
||||||
|
#
|
||||||
|
location ~ \.php$ {
|
||||||
|
try_files $uri =404;
|
||||||
|
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||||
|
fastcgi_pass 127.0.0.1:9000;
|
||||||
|
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||||
|
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
|
||||||
|
fastcgi_index index.php;
|
||||||
|
include fastcgi_params;
|
||||||
|
}
|
||||||
|
|
||||||
|
location ~* \.(jpg|jpeg|gif|png|css|js|ico|xml)$ {
|
||||||
|
expires 5d;
|
||||||
|
}
|
||||||
|
|
||||||
|
# deny access to . files, for security
|
||||||
|
#
|
||||||
|
location ~ /\. {
|
||||||
|
log_not_found off;
|
||||||
|
deny all;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /ws/map/update {
|
||||||
|
proxy_pass http://websocket;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection $connection_upgrade;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
|
||||||
|
proxy_set_header X-Forwarded-For $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-Host $host;
|
||||||
|
proxy_set_header X-Forwarded-Port $server_port;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
proxy_read_timeout 8h;
|
||||||
|
proxy_send_timeout 5s;
|
||||||
|
proxy_connect_timeout 3s;
|
||||||
|
proxy_buffering off;
|
||||||
|
}
|
||||||
|
# static sources
|
||||||
|
location /public/ {
|
||||||
|
sendfile on;
|
||||||
|
tcp_nopush on;
|
||||||
|
tcp_nodelay on;
|
||||||
|
keepalive_timeout 10s;
|
||||||
|
sendfile_max_chunk 512k;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
60
static/pathfinder/environment.ini
Normal file
60
static/pathfinder/environment.ini
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
; Environment Config
|
||||||
|
|
||||||
|
[ENVIRONMENT]
|
||||||
|
; project environment (DEVELOP || PRODUCTION).
|
||||||
|
; This effects: DB connection, Mail-Server, SSO, ESI configurations in this file
|
||||||
|
; configuration below
|
||||||
|
SERVER = PRODUCTION
|
||||||
|
|
||||||
|
[ENVIRONMENT.PRODUCTION]
|
||||||
|
; path to index.php (Default: leave blank == "auto-detect")
|
||||||
|
; -> e.g. set /pathfinder if your URL looks like https://www.[YOUR_DOMAIN]/pathfinder (subfolder)
|
||||||
|
BASE =
|
||||||
|
; deployment URL (e.g. https://www.pathfinder-w.space)
|
||||||
|
URL = {{@SCHEME}}://$DOMAIN
|
||||||
|
; level of debug/error stack trace
|
||||||
|
DEBUG = 0
|
||||||
|
; Pathfinder database
|
||||||
|
DB_PF_DNS = mysql:host=${CONTAINER_NAME}db;port=3306;dbname=
|
||||||
|
DB_PF_NAME = pf
|
||||||
|
DB_PF_USER = root
|
||||||
|
DB_PF_PASS = $MYSQL_PASSWORD
|
||||||
|
|
||||||
|
; Universe data (New Eden) cache DB for ESI API respons
|
||||||
|
DB_UNIVERSE_DNS = mysql:host=${CONTAINER_NAME}db;port=3306;dbname=
|
||||||
|
DB_UNIVERSE_NAME = eve_universe
|
||||||
|
DB_UNIVERSE_USER = root
|
||||||
|
DB_UNIVERSE_PASS = $MYSQL_PASSWORD
|
||||||
|
|
||||||
|
|
||||||
|
; EVE-Online CCP Database export
|
||||||
|
DB_CCP_DNS = mysql:host=${CONTAINER_NAME}db;port=3306;dbname=
|
||||||
|
DB_CCP_NAME = eve_lifeblood_min
|
||||||
|
DB_CCP_USER = root
|
||||||
|
DB_CCP_PASS = $MYSQL_PASSWORD
|
||||||
|
|
||||||
|
; CCP SSO
|
||||||
|
CCP_SSO_URL = https://login.eveonline.com
|
||||||
|
CCP_SSO_CLIENT_ID = $CCP_SSO_CLIENT_ID
|
||||||
|
CCP_SSO_SECRET_KEY = $CCP_SSO_SECRET_KEY
|
||||||
|
CCP_SSO_DOWNTIME = 11:00
|
||||||
|
|
||||||
|
; CCP ESI API
|
||||||
|
CCP_ESI_URL = https://esi.evetech.net
|
||||||
|
CCP_ESI_DATASOURCE = tranquility
|
||||||
|
CCP_ESI_SCOPES = $CCP_ESI_SCOPES
|
||||||
|
CCP_ESI_SCOPES_ADMIN =
|
||||||
|
|
||||||
|
; SMTP settings (optional)
|
||||||
|
SMTP_HOST = localhost
|
||||||
|
SMTP_PORT = 25
|
||||||
|
SMTP_SCHEME = TLS
|
||||||
|
SMTP_USER =
|
||||||
|
SMTP_PASS =
|
||||||
|
|
||||||
|
SMTP_FROM = registration@pathfinder-w.space
|
||||||
|
SMTP_ERROR = admin@pathfinder-w.space
|
||||||
|
|
||||||
|
; TCP Socket configuration (optional) (advanced)
|
||||||
|
SOCKET_HOST = ${CONTAINER_NAME}-socket
|
||||||
|
SOCKET_PORT = 5555
|
||||||
27
static/pathfinder/routes.ini
Normal file
27
static/pathfinder/routes.ini
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
; Route config
|
||||||
|
|
||||||
|
[routes]
|
||||||
|
; DB setup setup
|
||||||
|
; IMPORTANT: remove/comment this line after setup/update is finished!
|
||||||
|
GET @setup: /setup [sync] = {{ @NAMESPACE }}\Controller\Setup->init
|
||||||
|
; login (index) page
|
||||||
|
GET @login: / [sync] = {{ @NAMESPACE }}\Controller\AppController->init
|
||||||
|
; CCP SSO redirect
|
||||||
|
GET @sso: /sso/@action [sync] = {{ @NAMESPACE }}\Controller\Ccp\Sso->@action
|
||||||
|
; map page
|
||||||
|
GET @map: /map* [sync] = {{ @NAMESPACE }}\Controller\MapController->init
|
||||||
|
; admin panel
|
||||||
|
GET @admin: /admin* [sync] = {{ @NAMESPACE }}\Controller\Admin->dispatch
|
||||||
|
|
||||||
|
; AJAX API wildcard endpoints (not cached, throttled)
|
||||||
|
GET|POST /api/@controller/@action [ajax] = {{ @NAMESPACE }}\Controller\Api\@controller->@action, 0, 512
|
||||||
|
GET|POST /api/@controller/@action/@arg1 [ajax] = {{ @NAMESPACE }}\Controller\Api\@controller->@action, 0, 512
|
||||||
|
GET|POST /api/@controller/@action/@arg1/@arg2 [ajax] = {{ @NAMESPACE }}\Controller\Api\@controller->@action, 0, 512
|
||||||
|
|
||||||
|
; onUnload route or final map sync (@see https://developer.mozilla.org/docs/Web/API/Navigator/sendBeacon)
|
||||||
|
POST /api/Map/updateUnloadData = {{ @NAMESPACE }}\Controller\Api\Map->updateUnloadData, 0, 512
|
||||||
|
|
||||||
|
[maps]
|
||||||
|
; REST API wildcard endpoints (not cached, throttled)
|
||||||
|
/api/rest/@controller* [ajax] = {{ @NAMESPACE }}\Controller\Api\Rest\@controller, 0, 512
|
||||||
|
/api/rest/@controller/@id [ajax] = {{ @NAMESPACE }}\Controller\Api\Rest\@controller, 0, 512
|
||||||
40
static/php/fpm-pool.conf
Executable file
40
static/php/fpm-pool.conf
Executable file
|
|
@ -0,0 +1,40 @@
|
||||||
|
[global]
|
||||||
|
; Log to stderr
|
||||||
|
error_log = /dev/stderr
|
||||||
|
|
||||||
|
[www]
|
||||||
|
user = nobody
|
||||||
|
group = nobody
|
||||||
|
; Enable status page
|
||||||
|
pm.status_path = /fpm-status
|
||||||
|
|
||||||
|
; Ondemand process manager
|
||||||
|
pm = ondemand
|
||||||
|
|
||||||
|
; The number of child processes to be created when pm is set to 'static' and the
|
||||||
|
; maximum number of child processes when pm is set to 'dynamic' or 'ondemand'.
|
||||||
|
; This value sets the limit on the number of simultaneous requests that will be
|
||||||
|
; served. Equivalent to the ApacheMaxClients directive with mpm_prefork.
|
||||||
|
; Equivalent to the PHP_FCGI_CHILDREN environment variable in the original PHP
|
||||||
|
; CGI. The below defaults are based on a server without much resources. Don't
|
||||||
|
; forget to tweak pm.* to fit your needs.
|
||||||
|
; Note: Used when pm is set to 'static', 'dynamic' or 'ondemand'
|
||||||
|
; Note: This value is mandatory.
|
||||||
|
pm.max_children = 50
|
||||||
|
|
||||||
|
; The number of seconds after which an idle process will be killed.
|
||||||
|
; Note: Used only when pm is set to 'ondemand'
|
||||||
|
; Default Value: 10s
|
||||||
|
pm.process_idle_timeout = 10s;
|
||||||
|
|
||||||
|
; The number of requests each child process should execute before respawning.
|
||||||
|
; This can be useful to work around memory leaks in 3rd party libraries. For
|
||||||
|
; endless request processing specify '0'. Equivalent to PHP_FCGI_MAX_REQUESTS.
|
||||||
|
; Default Value: 0
|
||||||
|
pm.max_requests = 500
|
||||||
|
|
||||||
|
; Make sure the FPM workers can reach the environment variables for configuration
|
||||||
|
clear_env = no
|
||||||
|
|
||||||
|
; Catch output from PHP
|
||||||
|
catch_workers_output = yes
|
||||||
14
static/php/php.ini
Executable file
14
static/php/php.ini
Executable file
|
|
@ -0,0 +1,14 @@
|
||||||
|
upload_max_filesize = 100M
|
||||||
|
post_max_size = 108M
|
||||||
|
max_input_vars = 3000
|
||||||
|
html_errors = 0
|
||||||
|
cgi.force_redirect=0
|
||||||
|
cgi.fix_pathinfo=1
|
||||||
|
fastcgi.impersonate=1
|
||||||
|
fastcgi.logging=0
|
||||||
|
request_terminate_timeout = 300
|
||||||
|
session.save_handler = redis
|
||||||
|
session.save_path = "tcp://${CONTAINER_NAME}-redis:6379"
|
||||||
|
[Date]
|
||||||
|
date.timezone="UTC"
|
||||||
|
|
||||||
24
static/supervisord.conf
Executable file
24
static/supervisord.conf
Executable file
|
|
@ -0,0 +1,24 @@
|
||||||
|
[supervisord]
|
||||||
|
nodaemon=true
|
||||||
|
[program:php-fpm]
|
||||||
|
command=php-fpm7 -F
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
|
autorestart=true
|
||||||
|
startretries=0
|
||||||
|
|
||||||
|
[program:nginx]
|
||||||
|
command=nginx -g 'daemon off;'
|
||||||
|
stdout_logfile=/dev/stdout
|
||||||
|
stdout_logfile_maxbytes=0
|
||||||
|
stderr_logfile=/dev/stderr
|
||||||
|
stderr_logfile_maxbytes=0
|
||||||
|
autorestart=true
|
||||||
|
startretries=0
|
||||||
|
|
||||||
|
[program:cron]
|
||||||
|
command = /usr/sbin/crond -f -l 8
|
||||||
|
autostart=true
|
||||||
|
autorestart=true
|
||||||
17
websocket/.gitattributes
vendored
Normal file
17
websocket/.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
# Auto detect text files and perform LF normalization
|
||||||
|
* text=auto
|
||||||
|
|
||||||
|
# Custom for Visual Studio
|
||||||
|
*.cs diff=csharp
|
||||||
|
|
||||||
|
# Standard to msysgit
|
||||||
|
*.doc diff=astextplain
|
||||||
|
*.DOC diff=astextplain
|
||||||
|
*.docx diff=astextplain
|
||||||
|
*.DOCX diff=astextplain
|
||||||
|
*.dot diff=astextplain
|
||||||
|
*.DOT diff=astextplain
|
||||||
|
*.pdf diff=astextplain
|
||||||
|
*.PDF diff=astextplain
|
||||||
|
*.rtf diff=astextplain
|
||||||
|
*.RTF diff=astextplain
|
||||||
11
websocket/.gitignore
vendored
Normal file
11
websocket/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
# Created by .ignore support plugin (hsz.mobi)
|
||||||
|
### Example user template template
|
||||||
|
### Example user template
|
||||||
|
|
||||||
|
# IntelliJ project files
|
||||||
|
.idea
|
||||||
|
out
|
||||||
|
gen
|
||||||
|
|
||||||
|
vendor
|
||||||
|
|
||||||
134
websocket/README.md
Normal file
134
websocket/README.md
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
## WebSocket server for [Pathfinder](https://github.com/exodus4d/pathfinder)
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
- _PHP_ (≥ v7.1)
|
||||||
|
- A working instance of *[Pathfinder](https://github.com/exodus4d/pathfinder)* (≥ v2.0.0-rc.1)
|
||||||
|
- [_Composer_](https://getcomposer.org/download/) to install packages for the WebSocket server
|
||||||
|
|
||||||
|
### Install
|
||||||
|
1. Checkout this project in a **new** folder e.g. `/var/www/websocket.pathfinder`
|
||||||
|
1. Install [_Composer_](https://getcomposer.org/download/)
|
||||||
|
2. Install Composer dependencies from `composer.json` file:
|
||||||
|
- `$ cd /var/www/websocket.pathfinder`
|
||||||
|
- `$ composer install`
|
||||||
|
3. Start WebSocket server `$ php cmd.php`
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
#### Default
|
||||||
|
|
||||||
|
**Clients (WebBrowser) listen for connections**
|
||||||
|
- Host: `0.0.0.0.` (=> any client can connect)
|
||||||
|
- Port: `8020`
|
||||||
|
- ↪ URI: `127.0.0.1:8020`
|
||||||
|
|
||||||
|
(=> Your WebServer (e.g. Nginx) should proxy all WebSocket connections to this source)
|
||||||
|
|
||||||
|
**TCP TcpSocket connection (Internal use for WebServer ⇄ WebSocket server communication)**
|
||||||
|
- Host: `127.0.0.1` (=> Assumed WebServer and WebSocket server running on the same machine)
|
||||||
|
- Port: `5555`
|
||||||
|
- ↪ URI: `tcp://127.0.0.1:5555`
|
||||||
|
|
||||||
|
(=> Where _Pathfinder_ reaches the WebSocket server. This must match `SOCKET_HOST`, `SOCKET_PORT` options in `environment.ini`)
|
||||||
|
|
||||||
|
#### Start parameters [Optional]
|
||||||
|
|
||||||
|
The default configuration should be fine for most installations.
|
||||||
|
You can change/overwrite the default **Host** and **Port** configuration by adding additional CLI parameters when starting the WebSocket server:
|
||||||
|
|
||||||
|
`$ php cmd.php --wsHost [CLIENTS_HOST] --wsPort [CLIENTS_PORT] --tcpHost [TCP_HOST] --tcpPort [TCP_PORT] --debug 0`
|
||||||
|
|
||||||
|
For example: If you want to change the the WebSocket port and increase debug output:
|
||||||
|
|
||||||
|
`$ php cmd.php --wsPort 8030 --debug 3`
|
||||||
|
|
||||||
|
##### --debug (default `--debug 2`)
|
||||||
|
|
||||||
|
Allows you to set log output level from `0` (silent) - errors are not logged, to `3` (debug) for detailed logging.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### WebSocket UI
|
||||||
|
|
||||||
|
There is a WebSocket section on _Pathinders_ `/setup` page. After the WebSocket server is started, you should check it if everything works.
|
||||||
|
You see the most recent WebSocket log entries, the current connection state, the current number of active connections and all maps that have subscriptions
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Log entry view. Depending on the `--debug` parameter, the most recent (max 50) entries will be shown:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Subscriptions for each map:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Unix Service (systemd)
|
||||||
|
|
||||||
|
#### New Service
|
||||||
|
It is recommended to wrap the `cmd.php` script in a Unix service, that over control the WebSocket server.
|
||||||
|
This creates a systemd service on CentOS7:
|
||||||
|
1. `$ cd /etc/systemd/system`
|
||||||
|
2. `$ vi websocket.pathfinder.service`
|
||||||
|
3. Copy script and adjust `ExecStart` and `WorkingDirectory` values:
|
||||||
|
|
||||||
|
```
|
||||||
|
[Unit]
|
||||||
|
Description = WebSocket server (Pathfinder) [LIVE] environment
|
||||||
|
After = multi-user.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type = idle
|
||||||
|
ExecStart = /usr/bin/php /var/www/websocket.pathfinder/pathfinder_websocket/cmd.php
|
||||||
|
WorkingDirectory = /var/www/websocket.pathfinder/pathfinder_websocket
|
||||||
|
TimeoutStopSec = 0
|
||||||
|
Restart = always
|
||||||
|
LimitNOFILE = 10000
|
||||||
|
Nice = 10
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy = multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
Now you can use the service to start/stop/restart your WebSocket server
|
||||||
|
- `$ systemctl start websocket.pathfinder.service`
|
||||||
|
- `$ systemctl restart websocket.pathfinder.service`
|
||||||
|
- `$ systemctl stop websocket.pathfinder.service`
|
||||||
|
|
||||||
|
#### Auto-Restart the Service
|
||||||
|
You can automatically restart your service (e.g. on _EVE-Online_ downtime). Create a new "timer" for the automatic restart.
|
||||||
|
1. `$ cd /etc/systemd/system` (same dir as before)
|
||||||
|
2. `$ vi restart.websocket.pathfinder.timer`
|
||||||
|
3. Copy script:
|
||||||
|
|
||||||
|
```
|
||||||
|
[Unit]
|
||||||
|
Description = Restart timer (EVE downtime) for WebSocket server [LIVE]
|
||||||
|
|
||||||
|
[Timer]
|
||||||
|
OnCalendar = *-*-* 12:01:00
|
||||||
|
Persistent = true
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy = timer.target
|
||||||
|
```
|
||||||
|
Now we need a new "restart service" for the timer:
|
||||||
|
1. `$ cd /etc/systemd/system` (same dir as before)
|
||||||
|
2. `$ vi restart.websocket.pathfinder.service`
|
||||||
|
3. Copy script:
|
||||||
|
|
||||||
|
```
|
||||||
|
[Unit]
|
||||||
|
Description = Restart (periodically) WebSocket server [LIVE]
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type = oneshot
|
||||||
|
ExecStart = /usr/bin/systemctl try-restart websocket.pathfinder.service
|
||||||
|
```
|
||||||
|
And then, we need to either restart the machine or launch
|
||||||
|
```
|
||||||
|
systemctl start restart.websocket.pathfinder.timer
|
||||||
|
```
|
||||||
|
### Info
|
||||||
|
- [*Ratchet*](http://socketo.me) - "WebSockets for PHP"
|
||||||
|
- [*ReactPHP*](https://reactphp.org) - "Event-driven, non-blocking I/O with PHP"
|
||||||
269
websocket/app/Component/AbstractMessageComponent.php
Normal file
269
websocket/app/Component/AbstractMessageComponent.php
Normal file
|
|
@ -0,0 +1,269 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace Exodus4D\Socket\Component;
|
||||||
|
|
||||||
|
|
||||||
|
use Exodus4D\Socket\Data\Payload;
|
||||||
|
use Exodus4D\Socket\Log\Store;
|
||||||
|
use Ratchet\ConnectionInterface;
|
||||||
|
use Ratchet\MessageComponentInterface;
|
||||||
|
use React\EventLoop\TimerInterface;
|
||||||
|
|
||||||
|
abstract class AbstractMessageComponent implements MessageComponentInterface {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* unique name for this component
|
||||||
|
* -> should be overwritten in child instances
|
||||||
|
* -> is used as "log store" name
|
||||||
|
*/
|
||||||
|
const COMPONENT_NAME = 'default';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* log message server start
|
||||||
|
*/
|
||||||
|
const LOG_TEXT_SERVER_START = 'start WebSocket server…';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* store for logs
|
||||||
|
* @var Store
|
||||||
|
*/
|
||||||
|
protected $logStore;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* stores all active connections
|
||||||
|
* -> regardless of its subscription state
|
||||||
|
* [
|
||||||
|
* '$conn1->resourceId' => [
|
||||||
|
* 'connection' => $conn1,
|
||||||
|
* 'data' => null
|
||||||
|
* ],
|
||||||
|
* '$conn2->resourceId' => [
|
||||||
|
* 'connection' => $conn2,
|
||||||
|
* 'data' => null
|
||||||
|
* ]
|
||||||
|
* ]
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private $connections;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* max count of concurrent open connections
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private $maxConnections = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AbstractMessageComponent constructor.
|
||||||
|
* @param Store $store
|
||||||
|
*/
|
||||||
|
public function __construct(Store $store){
|
||||||
|
$this->connections = [];
|
||||||
|
$this->logStore = $store;
|
||||||
|
|
||||||
|
$this->log(['debug', 'info'], null, 'START', static::LOG_TEXT_SERVER_START);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connection callbacks from MessageComponentInterface ============================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* new client connection onOpen
|
||||||
|
* @param ConnectionInterface $conn
|
||||||
|
*/
|
||||||
|
public function onOpen(ConnectionInterface $conn){
|
||||||
|
$this->log(['debug'], $conn, __FUNCTION__, 'open connection');
|
||||||
|
|
||||||
|
$this->addConnection($conn);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* client connection onClose
|
||||||
|
* @param ConnectionInterface $conn
|
||||||
|
*/
|
||||||
|
public function onClose(ConnectionInterface $conn){
|
||||||
|
$this->log(['debug'], $conn, __FUNCTION__, 'close connection');
|
||||||
|
|
||||||
|
$this->removeConnection($conn);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* client connection onError
|
||||||
|
* @param ConnectionInterface $conn
|
||||||
|
* @param \Exception $e
|
||||||
|
*/
|
||||||
|
public function onError(ConnectionInterface $conn, \Exception $e){
|
||||||
|
$this->log(['debug', 'error'], $conn, __FUNCTION__, $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* new message received from client connection
|
||||||
|
* @param ConnectionInterface $conn
|
||||||
|
* @param string $msg
|
||||||
|
*/
|
||||||
|
public function onMessage(ConnectionInterface $conn, $msg){
|
||||||
|
// parse message into payload object
|
||||||
|
$payload = $this->getPayloadFromMessage($msg);
|
||||||
|
|
||||||
|
if($payload){
|
||||||
|
$this->dispatchWebSocketPayload($conn, $payload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connection handling ============================================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* add connection
|
||||||
|
* @param ConnectionInterface $conn
|
||||||
|
*/
|
||||||
|
private function addConnection(ConnectionInterface $conn) : void {
|
||||||
|
$this->connections[$conn->resourceId] = [
|
||||||
|
'connection' => $conn,
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->maxConnections = max(count($this->connections), $this->maxConnections);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* remove connection
|
||||||
|
* @param ConnectionInterface $conn
|
||||||
|
*/
|
||||||
|
private function removeConnection(ConnectionInterface $conn) : void {
|
||||||
|
if($this->hasConnection($conn)){
|
||||||
|
unset($this->connections[$conn->resourceId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ConnectionInterface $conn
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected function hasConnection(ConnectionInterface $conn) : bool {
|
||||||
|
return isset($this->connections[$conn->resourceId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $resourceId
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected function hasConnectionId(int $resourceId) : bool {
|
||||||
|
return isset($this->connections[$resourceId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $resourceId
|
||||||
|
* @return ConnectionInterface|null
|
||||||
|
*/
|
||||||
|
protected function getConnection(int $resourceId) : ?ConnectionInterface {
|
||||||
|
return $this->hasConnectionId($resourceId) ? $this->connections[$resourceId]['connection'] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* update meta data for $conn
|
||||||
|
* @param ConnectionInterface $conn
|
||||||
|
*/
|
||||||
|
protected function updateConnection(ConnectionInterface $conn){
|
||||||
|
if($this->hasConnection($conn)){
|
||||||
|
$meta = [
|
||||||
|
'mTimeSend' => microtime(true)
|
||||||
|
];
|
||||||
|
$this->connections[$conn->resourceId]['data'] = array_merge($this->getConnectionData($conn), $meta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get meta data from $conn
|
||||||
|
* @param ConnectionInterface $conn
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected function getConnectionData(ConnectionInterface $conn) : array {
|
||||||
|
$meta = [];
|
||||||
|
if($this->hasConnection($conn)){
|
||||||
|
$meta = (array)$this->connections[$conn->resourceId]['data'];
|
||||||
|
}
|
||||||
|
return $meta;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* wrapper for ConnectionInterface->send()
|
||||||
|
* -> this stores some meta data to the $conn
|
||||||
|
* @param ConnectionInterface $conn
|
||||||
|
* @param $data
|
||||||
|
*/
|
||||||
|
protected function send(ConnectionInterface $conn, $data){
|
||||||
|
$conn->send($data);
|
||||||
|
$this->updateConnection($conn);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ConnectionInterface $conn
|
||||||
|
* @param Payload $payload
|
||||||
|
*/
|
||||||
|
abstract protected function dispatchWebSocketPayload(ConnectionInterface $conn, Payload $payload) : void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get Payload class from client message
|
||||||
|
* @param mixed $msg
|
||||||
|
* @return Payload|null
|
||||||
|
*/
|
||||||
|
protected function getPayloadFromMessage($msg) : ?Payload {
|
||||||
|
$payload = null;
|
||||||
|
$msg = (array)json_decode($msg, true);
|
||||||
|
|
||||||
|
if(isset($msg['task'], $msg['load'])){
|
||||||
|
$payload = $this->newPayload((string)$msg['task'], $msg['load']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $task
|
||||||
|
* @param null $load
|
||||||
|
* @param array|null $characterIds
|
||||||
|
* @return Payload|null
|
||||||
|
*/
|
||||||
|
protected function newPayload(string $task, $load = null, ?array $characterIds = null) : ?Payload {
|
||||||
|
$payload = null;
|
||||||
|
try{
|
||||||
|
$payload = new Payload($task, $load, $characterIds);
|
||||||
|
}catch(\Exception $e){
|
||||||
|
$this->log(['debug', 'error'], null, __FUNCTION__, $e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return $payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get WebSocket stats data
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getSocketStats() : array {
|
||||||
|
return [
|
||||||
|
'connections' => count($this->connections),
|
||||||
|
'maxConnections' => $this->maxConnections,
|
||||||
|
'logs' => array_reverse($this->logStore->getStore())
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $logTypes
|
||||||
|
* @param ConnectionInterface|null $connection
|
||||||
|
* @param string $action
|
||||||
|
* @param string $message
|
||||||
|
*/
|
||||||
|
protected function log($logTypes, ?ConnectionInterface $connection, string $action, string $message = '') : void {
|
||||||
|
if($this->logStore){
|
||||||
|
$remoteAddress = $connection ? $connection->remoteAddress : null;
|
||||||
|
$resourceId = $connection ? $connection->resourceId : null;
|
||||||
|
$this->logStore->log($logTypes, $remoteAddress, $resourceId, $action, $message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param TimerInterface $timer
|
||||||
|
*/
|
||||||
|
public function housekeeping(TimerInterface $timer) : void {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
41
websocket/app/Component/Formatter/SubscriptionFormatter.php
Normal file
41
websocket/app/Component/Formatter/SubscriptionFormatter.php
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Created by PhpStorm.
|
||||||
|
* User: exodu
|
||||||
|
* Date: 31.03.2018
|
||||||
|
* Time: 13:09
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Exodus4D\Socket\Component\Formatter;
|
||||||
|
|
||||||
|
|
||||||
|
class SubscriptionFormatter{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* group charactersData by systemId based on their current 'log' data
|
||||||
|
* @param array $charactersData
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
static function groupCharactersDataBySystem(array $charactersData) : array {
|
||||||
|
$data = [];
|
||||||
|
foreach($charactersData as $characterId => $characterData){
|
||||||
|
// check if characterData has an active log (active system for character)
|
||||||
|
$systemId = 0;
|
||||||
|
if(isset($characterData['log']['system']['id'])){
|
||||||
|
$systemId = (int)$characterData['log']['system']['id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if( !isset($data[$systemId]) ){
|
||||||
|
$systemData = (object)[];
|
||||||
|
$systemData->id = $systemId;
|
||||||
|
$data[$systemId] = $systemData;
|
||||||
|
}
|
||||||
|
|
||||||
|
$data[$systemId]->user[] = $characterData;
|
||||||
|
}
|
||||||
|
$data = array_values($data);
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
76
websocket/app/Component/Handler/LogFileHandler.php
Normal file
76
websocket/app/Component/Handler/LogFileHandler.php
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Created by PhpStorm.
|
||||||
|
* User: exodu
|
||||||
|
* Date: 03.09.2017
|
||||||
|
* Time: 17:02
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Exodus4D\Socket\Component\Handler;
|
||||||
|
|
||||||
|
|
||||||
|
class LogFileHandler {
|
||||||
|
|
||||||
|
const ERROR_DIR_CREATE = 'There is no existing directory at "%s" and its not buildable.';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* steam uri
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $stream = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* stream dir
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $dir = '.';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* file base dir already created
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
private $dirCreated = false;
|
||||||
|
|
||||||
|
public function __construct(string $stream){
|
||||||
|
$this->stream = $stream;
|
||||||
|
$this->dir = dirname($this->stream);
|
||||||
|
$this->createDir();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* write log data into to file
|
||||||
|
* @param array $log
|
||||||
|
*/
|
||||||
|
public function write(array $log){
|
||||||
|
$log = (string)json_encode($log, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||||
|
if( !empty($log) ){
|
||||||
|
if($stream = fopen($this->stream, 'a')){
|
||||||
|
flock($stream, LOCK_EX);
|
||||||
|
fwrite($stream, $log . PHP_EOL);
|
||||||
|
flock($stream, LOCK_UN);
|
||||||
|
fclose($stream);
|
||||||
|
|
||||||
|
// logs should be writable for non webSocket user too
|
||||||
|
@chmod($this->stream, 0666);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* create directory
|
||||||
|
*/
|
||||||
|
private function createDir(){
|
||||||
|
// Do not try to create dir if it has already been tried.
|
||||||
|
if ($this->dirCreated){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->dir && !is_dir($this->dir)){
|
||||||
|
$status = mkdir($this->dir, 0777, true);
|
||||||
|
if (false === $status) {
|
||||||
|
throw new \UnexpectedValueException(sprintf(self::ERROR_DIR_CREATE, $this->dir));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->dirCreated = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
943
websocket/app/Component/MapUpdate.php
Normal file
943
websocket/app/Component/MapUpdate.php
Normal file
|
|
@ -0,0 +1,943 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Created by PhpStorm.
|
||||||
|
* User: Exodus
|
||||||
|
* Date: 02.12.2016
|
||||||
|
* Time: 22:29
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Exodus4D\Socket\Component;
|
||||||
|
|
||||||
|
use Exodus4D\Socket\Component\Handler\LogFileHandler;
|
||||||
|
use Exodus4D\Socket\Component\Formatter\SubscriptionFormatter;
|
||||||
|
use Exodus4D\Socket\Data\Payload;
|
||||||
|
use Exodus4D\Socket\Log\Store;
|
||||||
|
use Ratchet\ConnectionInterface;
|
||||||
|
|
||||||
|
class MapUpdate extends AbstractMessageComponent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* unique name for this component
|
||||||
|
* -> should be overwritten in child instances
|
||||||
|
* -> is used as "log store" name
|
||||||
|
*/
|
||||||
|
const COMPONENT_NAME = 'webSock';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* log message unknown task name
|
||||||
|
*/
|
||||||
|
const LOG_TEXT_TASK_UNKNOWN = 'unknown task: %s';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* log message for denied subscription attempt. -> character data unknown
|
||||||
|
*/
|
||||||
|
const LOG_TEXT_SUBSCRIBE_DENY = 'sub. denied for charId: %d';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* log message for invalid subscription data
|
||||||
|
*/
|
||||||
|
const LOG_TEXT_SUBSCRIBE_INVALID = 'sub. data invalid';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* log message for subscribe characterId
|
||||||
|
*/
|
||||||
|
const LOG_TEXT_SUBSCRIBE = 'sub. charId: %s to mapIds: [%s]';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* log message unsubscribe characterId
|
||||||
|
*/
|
||||||
|
const LOG_TEXT_UNSUBSCRIBE = 'unsub. charId: %d from mapIds: [%s]';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* log message for map data updated broadcast
|
||||||
|
*/
|
||||||
|
const LOG_TEXT_MAP_UPDATE = 'update map data, mapId: %d → broadcast to %d connections';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* log message for map subscriptions data updated broadcast
|
||||||
|
*/
|
||||||
|
const LOG_TEXT_MAP_SUBSCRIPTIONS = 'update map subscriptions data, mapId: %d. → broadcast to %d connections';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* log message for delete mapId broadcast
|
||||||
|
*/
|
||||||
|
const LOG_TEXT_MAP_DELETE = 'delete mapId: $d → broadcast to %d connections';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* timestamp (ms) from last healthCheck ping
|
||||||
|
* -> timestamp received from remote TCP socket
|
||||||
|
* @var int|null
|
||||||
|
*/
|
||||||
|
protected $healthCheckToken;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* expire time for map access tokens (seconds)
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
protected $mapAccessExpireSeconds = 30;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* character access tokens for clients
|
||||||
|
* -> tokens are unique and expire onSubscribe!
|
||||||
|
* [
|
||||||
|
* 'charId_1' => [
|
||||||
|
* [
|
||||||
|
* 'token' => $characterToken1,
|
||||||
|
* 'expire' => $expireTime1,
|
||||||
|
* 'characterData' => $characterData1
|
||||||
|
* ],
|
||||||
|
* [
|
||||||
|
* 'token' => $characterToken2,
|
||||||
|
* 'expire' => $expireTime2,
|
||||||
|
* 'characterData' => $characterData1
|
||||||
|
* ]
|
||||||
|
* ],
|
||||||
|
* 'charId_2' => [
|
||||||
|
* [
|
||||||
|
* 'token' => $characterToken3,
|
||||||
|
* 'expire' => $expireTime3,
|
||||||
|
* 'characterData' => $characterData2
|
||||||
|
* ]
|
||||||
|
* ]
|
||||||
|
* ]
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $characterAccessData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* access tokens for clients grouped by mapId
|
||||||
|
* -> tokens are unique and expire onSubscribe!
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $mapAccessData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* connected characters
|
||||||
|
* [
|
||||||
|
* 'charId_1' => [
|
||||||
|
* '$conn1->resourceId' => $conn1,
|
||||||
|
* '$conn2->resourceId' => $conn2
|
||||||
|
* ],
|
||||||
|
* 'charId_2' => [
|
||||||
|
* '$conn1->resourceId' => $conn1,
|
||||||
|
* '$conn3->resourceId' => $conn3
|
||||||
|
* ]
|
||||||
|
* ]
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $characters;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* valid client connections subscribed to maps
|
||||||
|
* [
|
||||||
|
* 'mapId_1' => [
|
||||||
|
* 'charId_1' => $charId_1,
|
||||||
|
* 'charId_2' => $charId_2
|
||||||
|
* ],
|
||||||
|
* 'mapId_2' => [
|
||||||
|
* 'charId_1' => $charId_1,
|
||||||
|
* 'charId_3' => $charId_3
|
||||||
|
* ]
|
||||||
|
* ]
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $subscriptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* collection of characterData for valid subscriptions
|
||||||
|
* [
|
||||||
|
* 'charId_1' => $characterData1,
|
||||||
|
* 'charId_2' => $characterData2
|
||||||
|
* ]
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $characterData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MapUpdate constructor.
|
||||||
|
* @param Store $store
|
||||||
|
*/
|
||||||
|
public function __construct(Store $store){
|
||||||
|
parent::__construct($store);
|
||||||
|
|
||||||
|
$this->characterAccessData = [];
|
||||||
|
$this->mapAccessData = [];
|
||||||
|
$this->characters = [];
|
||||||
|
$this->subscriptions = [];
|
||||||
|
$this->characterData = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* new client connection
|
||||||
|
* @param ConnectionInterface $conn
|
||||||
|
*/
|
||||||
|
public function onOpen(ConnectionInterface $conn){
|
||||||
|
parent::onOpen($conn);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ConnectionInterface $conn
|
||||||
|
*/
|
||||||
|
public function onClose(ConnectionInterface $conn){
|
||||||
|
parent::onClose($conn);
|
||||||
|
|
||||||
|
$this->unSubscribeConnection($conn);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ConnectionInterface $conn
|
||||||
|
* @param \Exception $e
|
||||||
|
*/
|
||||||
|
public function onError(ConnectionInterface $conn, \Exception $e){
|
||||||
|
parent::onError($conn, $e);
|
||||||
|
|
||||||
|
// close connection should trigger the onClose() callback for unSubscribe
|
||||||
|
$conn->close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ConnectionInterface $conn
|
||||||
|
* @param string $msg
|
||||||
|
*/
|
||||||
|
public function onMessage(ConnectionInterface $conn, $msg){
|
||||||
|
parent::onMessage($conn, $msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ConnectionInterface $conn
|
||||||
|
* @param Payload $payload
|
||||||
|
*/
|
||||||
|
protected function dispatchWebSocketPayload(ConnectionInterface $conn, Payload $payload) : void {
|
||||||
|
switch($payload->task){
|
||||||
|
case 'healthCheck':
|
||||||
|
$this->broadcastHealthCheck($conn, $payload);
|
||||||
|
break;
|
||||||
|
case 'subscribe':
|
||||||
|
$this->subscribe($conn, (array)$payload->load);
|
||||||
|
break;
|
||||||
|
case 'unsubscribe':
|
||||||
|
// make sure characterIds got from client are valid
|
||||||
|
// -> intersect with subscribed characterIds for current $conn
|
||||||
|
$characterIds = array_intersect((array)$payload->load, $this->getCharacterIdsByConnection($conn));
|
||||||
|
if(!empty($characterIds)){
|
||||||
|
$this->unSubscribeCharacterIds($characterIds, $conn);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$this->log(['debug', 'error'], $conn, __FUNCTION__, sprintf(static::LOG_TEXT_TASK_UNKNOWN, $payload->task));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* checks healthCheck $token and respond with validation status + subscription stats
|
||||||
|
* @param ConnectionInterface $conn
|
||||||
|
* @param Payload $payload
|
||||||
|
*/
|
||||||
|
private function broadcastHealthCheck(ConnectionInterface $conn, Payload $payload) : void {
|
||||||
|
$isValid = $this->validateHealthCheckToken((int)$payload->load);
|
||||||
|
|
||||||
|
$load = [
|
||||||
|
'isValid' => $isValid,
|
||||||
|
];
|
||||||
|
|
||||||
|
// Make sure WebSocket client request is valid
|
||||||
|
if($isValid){
|
||||||
|
// set new healthCheckToken for next check
|
||||||
|
$load['token'] = $this->setHealthCheckToken(microtime(true));
|
||||||
|
|
||||||
|
// add subscription stats if $token is valid
|
||||||
|
$load['subStats'] = $this->getSubscriptionStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload->setLoad($load);
|
||||||
|
|
||||||
|
$connections = new \SplObjectStorage();
|
||||||
|
$connections->attach($conn);
|
||||||
|
|
||||||
|
$this->broadcast($connections, $payload);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* compare token (timestamp from initial TCP healthCheck message) with token send from WebSocket
|
||||||
|
* @param int $token
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private function validateHealthCheckToken(int $token) : bool {
|
||||||
|
$isValid = false;
|
||||||
|
|
||||||
|
if($token && $this->healthCheckToken && $token === (int)$this->healthCheckToken){
|
||||||
|
$isValid = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// reset token
|
||||||
|
$this->healthCheckToken = null;
|
||||||
|
|
||||||
|
return $isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* subscribes a connection to valid accessible maps
|
||||||
|
* @param ConnectionInterface $conn
|
||||||
|
* @param $subscribeData
|
||||||
|
*/
|
||||||
|
private function subscribe(ConnectionInterface $conn, array $subscribeData) : void {
|
||||||
|
$characterId = (int)$subscribeData['id'];
|
||||||
|
$characterToken = (string)$subscribeData['token'];
|
||||||
|
|
||||||
|
if($characterId && $characterToken){
|
||||||
|
// check if character access token is valid (exists and not expired in $this->characterAccessData)
|
||||||
|
if($characterData = $this->checkCharacterAccess($characterId, $characterToken)){
|
||||||
|
$this->characters[$characterId][$conn->resourceId] = $conn;
|
||||||
|
|
||||||
|
// insert/update characterData cache
|
||||||
|
// -> even if characterId does not have access to a map "yet"
|
||||||
|
// -> no maps found but character can get map access at any time later
|
||||||
|
$this->setCharacterData($characterData);
|
||||||
|
|
||||||
|
// valid character -> check map access
|
||||||
|
$changedSubscriptionsMapIds = [];
|
||||||
|
foreach((array)$subscribeData['mapData'] as $data){
|
||||||
|
$mapId = (int)$data['id'];
|
||||||
|
$mapToken = (string)$data['token'];
|
||||||
|
$mapName = (string)$data['name'];
|
||||||
|
|
||||||
|
if($mapId && $mapToken){
|
||||||
|
// check if token is valid (exists and not expired) in $this->mapAccessData
|
||||||
|
if($this->checkMapAccess($characterId, $mapId, $mapToken)){
|
||||||
|
// valid map subscribe request
|
||||||
|
$this->subscriptions[$mapId]['characterIds'][$characterId] = $characterId;
|
||||||
|
$this->subscriptions[$mapId]['data']['name'] = $mapName;
|
||||||
|
$changedSubscriptionsMapIds[] = $mapId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sort($changedSubscriptionsMapIds, SORT_NUMERIC);
|
||||||
|
|
||||||
|
$this->log(['debug', 'info'], $conn, __FUNCTION__,
|
||||||
|
sprintf(static::LOG_TEXT_SUBSCRIBE, $characterId, implode(',', $changedSubscriptionsMapIds))
|
||||||
|
);
|
||||||
|
|
||||||
|
// broadcast all active subscriptions to subscribed connections -------------------------------------------
|
||||||
|
$this->broadcastMapSubscriptions($changedSubscriptionsMapIds);
|
||||||
|
}else{
|
||||||
|
$this->log(['debug', 'info'], $conn, __FUNCTION__, sprintf(static::LOG_TEXT_SUBSCRIBE_DENY, $characterId));
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
$this->log(['debug', 'error'], $conn, __FUNCTION__, static::LOG_TEXT_SUBSCRIBE_INVALID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* subscribes an active connection from maps
|
||||||
|
* @param ConnectionInterface $conn
|
||||||
|
*/
|
||||||
|
private function unSubscribeConnection(ConnectionInterface $conn){
|
||||||
|
$characterIds = $this->getCharacterIdsByConnection($conn);
|
||||||
|
$this->unSubscribeCharacterIds($characterIds, $conn);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* unSubscribe a $characterId from ALL maps
|
||||||
|
* -> if $conn is set -> just unSub the $characterId from this $conn
|
||||||
|
* @param int $characterId
|
||||||
|
* @param ConnectionInterface|null $conn
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private function unSubscribeCharacterId(int $characterId, ?ConnectionInterface $conn = null) : bool {
|
||||||
|
if($characterId){
|
||||||
|
// unSub from $this->characters ---------------------------------------------------------------------------
|
||||||
|
if($conn){
|
||||||
|
// just unSub a specific connection (e.g. single browser window)
|
||||||
|
unset($this->characters[$characterId][$conn->resourceId]);
|
||||||
|
|
||||||
|
if( !count($this->characters[$characterId]) ){
|
||||||
|
// no connection left for this character
|
||||||
|
unset($this->characters[$characterId]);
|
||||||
|
}
|
||||||
|
// TODO unset $this->>$characterData if $characterId does not have any other map subscribed to
|
||||||
|
}else{
|
||||||
|
// unSub ALL connections from a character (e.g. multiple browsers)
|
||||||
|
unset($this->characters[$characterId]);
|
||||||
|
|
||||||
|
// unset characterData cache
|
||||||
|
$this->deleteCharacterData($characterId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// unSub from $this->subscriptions ------------------------------------------------------------------------
|
||||||
|
$changedSubscriptionsMapIds = [];
|
||||||
|
foreach($this->subscriptions as $mapId => $subData){
|
||||||
|
if(array_key_exists($characterId, (array)$subData['characterIds'])){
|
||||||
|
unset($this->subscriptions[$mapId]['characterIds'][$characterId]);
|
||||||
|
|
||||||
|
if( !count($this->subscriptions[$mapId]['characterIds']) ){
|
||||||
|
// no characters left on this map
|
||||||
|
unset($this->subscriptions[$mapId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$changedSubscriptionsMapIds[] = $mapId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sort($changedSubscriptionsMapIds, SORT_NUMERIC);
|
||||||
|
|
||||||
|
$this->log(['debug', 'info'], $conn, __FUNCTION__,
|
||||||
|
sprintf(static::LOG_TEXT_UNSUBSCRIBE, $characterId, implode(',', $changedSubscriptionsMapIds))
|
||||||
|
);
|
||||||
|
|
||||||
|
// broadcast all active subscriptions to subscribed connections -------------------------------------------
|
||||||
|
$this->broadcastMapSubscriptions($changedSubscriptionsMapIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* unSubscribe $characterIds from ALL maps
|
||||||
|
* -> if $conn is set -> just unSub the $characterId from this $conn
|
||||||
|
* @param int[] $characterIds
|
||||||
|
* @param ConnectionInterface|null $conn
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private function unSubscribeCharacterIds(array $characterIds, ?ConnectionInterface $conn = null) : bool {
|
||||||
|
$response = false;
|
||||||
|
foreach($characterIds as $characterId){
|
||||||
|
$response = $this->unSubscribeCharacterId($characterId, $conn);
|
||||||
|
}
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* delete mapId from subscriptions and broadcast "delete msg" to clients
|
||||||
|
* @param string $task
|
||||||
|
* @param int $mapId
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
private function deleteMapId(string $task, int $mapId) : int {
|
||||||
|
$connectionCount = $this->broadcastMapData($task, $mapId, $mapId);
|
||||||
|
|
||||||
|
// remove map from subscriptions
|
||||||
|
if(isset($this->subscriptions[$mapId])){
|
||||||
|
unset($this->subscriptions[$mapId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->log(['debug', 'info'], null, __FUNCTION__,
|
||||||
|
sprintf(static::LOG_TEXT_MAP_DELETE, $mapId, $connectionCount)
|
||||||
|
);
|
||||||
|
|
||||||
|
return $connectionCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get all mapIds a characterId has subscribed to
|
||||||
|
* @param int $characterId
|
||||||
|
* @return int[]
|
||||||
|
*/
|
||||||
|
private function getMapIdsByCharacterId(int $characterId) : array {
|
||||||
|
$mapIds = [];
|
||||||
|
foreach($this->subscriptions as $mapId => $subData) {
|
||||||
|
if(array_key_exists($characterId, (array)$subData['characterIds'])){
|
||||||
|
$mapIds[] = $mapId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $mapIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ConnectionInterface $conn
|
||||||
|
* @return int[]
|
||||||
|
*/
|
||||||
|
private function getCharacterIdsByConnection(ConnectionInterface $conn) : array {
|
||||||
|
$characterIds = [];
|
||||||
|
$resourceId = $conn->resourceId;
|
||||||
|
|
||||||
|
foreach($this->characters as $characterId => $resourceIDs){
|
||||||
|
if(
|
||||||
|
array_key_exists($resourceId, $resourceIDs) &&
|
||||||
|
!in_array($characterId, $characterIds)
|
||||||
|
){
|
||||||
|
$characterIds[] = $characterId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $characterIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $mapId
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function getCharacterIdsByMapId(int $mapId) : array {
|
||||||
|
$characterIds = [];
|
||||||
|
if(
|
||||||
|
array_key_exists($mapId, $this->subscriptions) &&
|
||||||
|
is_array($this->subscriptions[$mapId]['characterIds'])
|
||||||
|
){
|
||||||
|
$characterIds = array_keys($this->subscriptions[$mapId]['characterIds']);
|
||||||
|
}
|
||||||
|
return $characterIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get connections by $characterIds
|
||||||
|
* @param int[] $characterIds
|
||||||
|
* @return \SplObjectStorage
|
||||||
|
*/
|
||||||
|
private function getConnectionsByCharacterIds(array $characterIds) : \SplObjectStorage {
|
||||||
|
$connections = new \SplObjectStorage;
|
||||||
|
foreach($characterIds as $characterId){
|
||||||
|
$connections->addAll($this->getConnectionsByCharacterId($characterId));
|
||||||
|
}
|
||||||
|
return $connections;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get connections by $characterId
|
||||||
|
* @param int $characterId
|
||||||
|
* @return \SplObjectStorage
|
||||||
|
*/
|
||||||
|
private function getConnectionsByCharacterId(int $characterId) : \SplObjectStorage {
|
||||||
|
$connections = new \SplObjectStorage;
|
||||||
|
if(isset($this->characters[$characterId])){
|
||||||
|
foreach(array_keys($this->characters[$characterId]) as $resourceId){
|
||||||
|
if(
|
||||||
|
$this->hasConnectionId($resourceId) &&
|
||||||
|
!$connections->contains($conn = $this->getConnection($resourceId))
|
||||||
|
){
|
||||||
|
$connections->attach($conn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $connections;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* check character access against $this->characterAccessData whitelist
|
||||||
|
* @param $characterId
|
||||||
|
* @param $characterToken
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function checkCharacterAccess(int $characterId, string $characterToken) : array {
|
||||||
|
$characterData = [];
|
||||||
|
if( !empty($characterAccessData = (array)$this->characterAccessData[$characterId]) ){
|
||||||
|
// check expire for $this->characterAccessData -> check ALL characters and remove expired
|
||||||
|
foreach($characterAccessData as $i => $data){
|
||||||
|
$deleteToken = false;
|
||||||
|
|
||||||
|
if( ((int)$data['expire'] - time()) > 0 ){
|
||||||
|
// still valid -> check token
|
||||||
|
if($characterToken === $data['token']){
|
||||||
|
$characterData = $data['characterData'];
|
||||||
|
$deleteToken = true;
|
||||||
|
// NO break; here -> check other characterAccessData as well
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
// token expired
|
||||||
|
$deleteToken = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if($deleteToken){
|
||||||
|
unset($this->characterAccessData[$characterId][$i]);
|
||||||
|
// -> check if tokens for this charId is empty
|
||||||
|
if( empty($this->characterAccessData[$characterId]) ){
|
||||||
|
unset($this->characterAccessData[$characterId]);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $characterData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* check map access against $this->mapAccessData whitelist
|
||||||
|
* @param $characterId
|
||||||
|
* @param $mapId
|
||||||
|
* @param $mapToken
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private function checkMapAccess(int $characterId, int $mapId, string $mapToken) : bool {
|
||||||
|
$access = false;
|
||||||
|
if( !empty($mapAccessData = (array)$this->mapAccessData[$mapId][$characterId]) ){
|
||||||
|
foreach($mapAccessData as $i => $data){
|
||||||
|
$deleteToken = false;
|
||||||
|
// check expire for $this->mapAccessData -> check ALL characters and remove expired
|
||||||
|
if( ((int)$data['expire'] - time()) > 0 ){
|
||||||
|
// still valid -> check token
|
||||||
|
if($mapToken === $data['token']){
|
||||||
|
$access = true;
|
||||||
|
$deleteToken = true;
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
// token expired
|
||||||
|
$deleteToken = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if($deleteToken){
|
||||||
|
unset($this->mapAccessData[$mapId][$characterId][$i]);
|
||||||
|
// -> check if tokens for this charId is empty
|
||||||
|
if( empty($this->mapAccessData[$mapId][$characterId]) ){
|
||||||
|
unset($this->mapAccessData[$mapId][$characterId]);
|
||||||
|
// -> check if map has no access tokens left for characters
|
||||||
|
if( empty($this->mapAccessData[$mapId]) ){
|
||||||
|
unset($this->mapAccessData[$mapId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $access;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* broadcast $payload to $connections
|
||||||
|
* @param \SplObjectStorage $connections
|
||||||
|
* @param Payload $payload
|
||||||
|
*/
|
||||||
|
private function broadcast(\SplObjectStorage $connections, Payload $payload) : void {
|
||||||
|
$data = json_encode($payload);
|
||||||
|
foreach($connections as $conn){
|
||||||
|
$this->send($conn, $data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// custom calls ===================================================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* receive data from TCP socket (main App)
|
||||||
|
* -> send response back
|
||||||
|
* @param string $task
|
||||||
|
* @param null|int|array $load
|
||||||
|
* @return bool|float|int|null
|
||||||
|
*/
|
||||||
|
public function receiveData(string $task, $load = null){
|
||||||
|
$responseLoad = null;
|
||||||
|
|
||||||
|
switch($task){
|
||||||
|
case 'healthCheck':
|
||||||
|
$responseLoad = $this->setHealthCheckToken((float)$load);
|
||||||
|
break;
|
||||||
|
case 'characterUpdate':
|
||||||
|
$this->updateCharacterData((array)$load);
|
||||||
|
$mapIds = $this->getMapIdsByCharacterId((int)$load['id']);
|
||||||
|
$this->broadcastMapSubscriptions($mapIds);
|
||||||
|
break;
|
||||||
|
case 'characterLogout':
|
||||||
|
$responseLoad = $this->unSubscribeCharacterIds((array)$load);
|
||||||
|
break;
|
||||||
|
case 'mapConnectionAccess':
|
||||||
|
$responseLoad = $this->setConnectionAccess($load);
|
||||||
|
break;
|
||||||
|
case 'mapAccess':
|
||||||
|
$responseLoad = $this->setAccess($task, $load);
|
||||||
|
break;
|
||||||
|
case 'mapUpdate':
|
||||||
|
$responseLoad = $this->broadcastMapUpdate($task, (array)$load);
|
||||||
|
break;
|
||||||
|
case 'mapDeleted':
|
||||||
|
$responseLoad = $this->deleteMapId($task, (int)$load);
|
||||||
|
break;
|
||||||
|
case 'logData':
|
||||||
|
$this->handleLogData((array)$load['meta'], (array)$load['log']);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $responseLoad;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param float $token
|
||||||
|
* @return float
|
||||||
|
*/
|
||||||
|
private function setHealthCheckToken(float $token) : float {
|
||||||
|
$this->healthCheckToken = $token;
|
||||||
|
return $this->healthCheckToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array $characterData
|
||||||
|
*/
|
||||||
|
private function setCharacterData(array $characterData) : void {
|
||||||
|
if($characterId = (int)$characterData['id']){
|
||||||
|
$this->characterData[$characterId] = $characterData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $characterId
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function getCharacterData(int $characterId) : array {
|
||||||
|
return empty($this->characterData[$characterId]) ? [] : $this->characterData[$characterId];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array $characterIds
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function getCharactersData(array $characterIds) : array {
|
||||||
|
return array_filter($this->characterData, function($characterId) use($characterIds) {
|
||||||
|
return in_array($characterId, $characterIds);
|
||||||
|
}, ARRAY_FILTER_USE_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array $characterData
|
||||||
|
*/
|
||||||
|
private function updateCharacterData(array $characterData) : void {
|
||||||
|
$characterId = (int)$characterData['id'];
|
||||||
|
if($this->getCharacterData($characterId)){
|
||||||
|
$this->setCharacterData($characterData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $characterId
|
||||||
|
*/
|
||||||
|
private function deleteCharacterData(int $characterId) : void {
|
||||||
|
unset($this->characterData[$characterId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array $mapIds
|
||||||
|
*/
|
||||||
|
private function broadcastMapSubscriptions(array $mapIds) : void {
|
||||||
|
$mapIds = array_unique($mapIds);
|
||||||
|
|
||||||
|
foreach($mapIds as $mapId){
|
||||||
|
if(
|
||||||
|
!empty($characterIds = $this->getCharacterIdsByMapId($mapId)) &&
|
||||||
|
!empty($charactersData = $this->getCharactersData($characterIds))
|
||||||
|
){
|
||||||
|
$systems = SubscriptionFormatter::groupCharactersDataBySystem($charactersData);
|
||||||
|
|
||||||
|
$mapUserData = (object)[];
|
||||||
|
$mapUserData->config = (object)['id' => $mapId];
|
||||||
|
$mapUserData->data = (object)['systems' => $systems];
|
||||||
|
|
||||||
|
$connectionCount = $this->broadcastMapData('mapSubscriptions', $mapId, $mapUserData);
|
||||||
|
|
||||||
|
$this->log(['debug'], null, __FUNCTION__,
|
||||||
|
sprintf(static::LOG_TEXT_MAP_SUBSCRIPTIONS, $mapId, $connectionCount)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $task
|
||||||
|
* @param array $mapData
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
private function broadcastMapUpdate(string $task, array $mapData) : int {
|
||||||
|
$mapId = (int)$mapData['config']['id'];
|
||||||
|
$connectionCount = $this->broadcastMapData($task, $mapId, $mapData);
|
||||||
|
|
||||||
|
$this->log(['debug'], null, __FUNCTION__,
|
||||||
|
sprintf(static::LOG_TEXT_MAP_UPDATE, $mapId, $connectionCount)
|
||||||
|
);
|
||||||
|
|
||||||
|
return $connectionCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* send map data to ALL connected clients
|
||||||
|
* @param string $task
|
||||||
|
* @param int $mapId
|
||||||
|
* @param mixed $load
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
private function broadcastMapData(string $task, int $mapId, $load) : int {
|
||||||
|
$characterIds = $this->getCharacterIdsByMapId($mapId);
|
||||||
|
$connections = $this->getConnectionsByCharacterIds($characterIds);
|
||||||
|
|
||||||
|
$this->broadcast($connections, $this->newPayload($task, $load, $characterIds));
|
||||||
|
|
||||||
|
return count($connections);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* set/update map access for allowed characterIds
|
||||||
|
* @param string $task
|
||||||
|
* @param array $accessData
|
||||||
|
* @return int count of connected characters
|
||||||
|
*/
|
||||||
|
private function setAccess(string $task, $accessData) : int {
|
||||||
|
$newMapCharacterIds = [];
|
||||||
|
|
||||||
|
if($mapId = (int)$accessData['id']){
|
||||||
|
$mapName = (string)$accessData['name'];
|
||||||
|
$characterIds = (array)$accessData['characterIds'];
|
||||||
|
// check all charactersIds that have map access... --------------------------------------------------------
|
||||||
|
foreach($characterIds as $characterId){
|
||||||
|
// ... for at least ONE active connection ...
|
||||||
|
// ... and characterData cache exists for characterId
|
||||||
|
if(
|
||||||
|
!empty($this->characters[$characterId]) &&
|
||||||
|
!empty($this->getCharacterData($characterId))
|
||||||
|
){
|
||||||
|
$newMapCharacterIds[$characterId] = $characterId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$currentMapCharacterIds = (array)$this->subscriptions[$mapId]['characterIds'];
|
||||||
|
|
||||||
|
// broadcast "map delete" to no longer valid characters ---------------------------------------------------
|
||||||
|
$removedMapCharacterIds = array_keys(array_diff_key($currentMapCharacterIds, $newMapCharacterIds));
|
||||||
|
$removedMapCharacterConnections = $this->getConnectionsByCharacterIds($removedMapCharacterIds);
|
||||||
|
|
||||||
|
$this->broadcast($removedMapCharacterConnections, $this->newPayload($task, $mapId, $removedMapCharacterIds));
|
||||||
|
|
||||||
|
// update map subscriptions -------------------------------------------------------------------------------
|
||||||
|
if( !empty($newMapCharacterIds) ){
|
||||||
|
// set new characters that have map access (overwrites existing subscriptions for that map)
|
||||||
|
$this->subscriptions[$mapId]['characterIds'] = $newMapCharacterIds;
|
||||||
|
$this->subscriptions[$mapId]['data']['name'] = $mapName;
|
||||||
|
|
||||||
|
// check if subscriptions have changed
|
||||||
|
if( !$this->arraysEqualKeys($currentMapCharacterIds, $newMapCharacterIds) ){
|
||||||
|
$this->broadcastMapSubscriptions([$mapId]);
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
// no characters (left) on this map
|
||||||
|
unset($this->subscriptions[$mapId]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count($newMapCharacterIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* set map access data (whitelist) tokens for map access
|
||||||
|
* @param $connectionAccessData
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
private function setConnectionAccess($connectionAccessData){
|
||||||
|
$response = false;
|
||||||
|
$characterId = (int)$connectionAccessData['id'];
|
||||||
|
$characterData = $connectionAccessData['characterData'];
|
||||||
|
$characterToken = $connectionAccessData['token'];
|
||||||
|
|
||||||
|
if(
|
||||||
|
$characterId &&
|
||||||
|
$characterData &&
|
||||||
|
$characterToken
|
||||||
|
){
|
||||||
|
// expire time for character and map tokens
|
||||||
|
$expireTime = time() + $this->mapAccessExpireSeconds;
|
||||||
|
|
||||||
|
// tokens for character access
|
||||||
|
$this->characterAccessData[$characterId][] = [
|
||||||
|
'token' => $characterToken,
|
||||||
|
'expire' => $expireTime,
|
||||||
|
'characterData' => $characterData
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach((array)$connectionAccessData['mapData'] as $mapData){
|
||||||
|
$mapId = (int)$mapData['id'];
|
||||||
|
|
||||||
|
$this->mapAccessData[$mapId][$characterId][] = [
|
||||||
|
'token' => $mapData['token'],
|
||||||
|
'expire' => $expireTime
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = 'OK';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get stats data
|
||||||
|
* -> lists all channels, subscribed characters + connection info
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected function getSubscriptionStats() : array {
|
||||||
|
$uniqueConnections = [];
|
||||||
|
$uniqueSubscriptions = [];
|
||||||
|
$channelsStats = [];
|
||||||
|
|
||||||
|
foreach($this->subscriptions as $mapId => $subData){
|
||||||
|
$characterIds = $this->getCharacterIdsByMapId($mapId);
|
||||||
|
$uniqueMapConnections = [];
|
||||||
|
|
||||||
|
$channelStats = [
|
||||||
|
'channelId' => $mapId,
|
||||||
|
'channelName' => $subData['data']['name'],
|
||||||
|
'countSub' => count($characterIds),
|
||||||
|
'countCon' => 0,
|
||||||
|
'subscriptions' => []
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach($characterIds as $characterId){
|
||||||
|
$characterData = $this->getCharacterData($characterId);
|
||||||
|
$connections = $this->getConnectionsByCharacterId($characterId);
|
||||||
|
|
||||||
|
$characterStats = [
|
||||||
|
'characterId' => $characterId,
|
||||||
|
'characterName' => isset($characterData['name']) ? $characterData['name'] : null,
|
||||||
|
'countCon' => $connections->count(),
|
||||||
|
'connections' => []
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach($connections as $connection){
|
||||||
|
if(!in_array($connection->resourceId, $uniqueMapConnections)){
|
||||||
|
$uniqueMapConnections[] = $connection->resourceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
$metaData = $this->getConnectionData($connection);
|
||||||
|
$microTime = (float)$metaData['mTimeSend'];
|
||||||
|
$logTime = Store::getDateTimeFromMicrotime($microTime);
|
||||||
|
|
||||||
|
$characterStats['connections'][] = [
|
||||||
|
'resourceId' => $connection->resourceId,
|
||||||
|
'remoteAddress' => $connection->remoteAddress,
|
||||||
|
'mTimeSend' => $microTime,
|
||||||
|
'mTimeSendFormat1' => $logTime->format('Y-m-d H:i:s.u'),
|
||||||
|
'mTimeSendFormat2' => $logTime->format('H:i:s')
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$channelStats['subscriptions'][] = $characterStats;
|
||||||
|
}
|
||||||
|
|
||||||
|
$uniqueConnections = array_unique(array_merge($uniqueConnections, $uniqueMapConnections));
|
||||||
|
$uniqueSubscriptions = array_unique(array_merge($uniqueSubscriptions, $characterIds));
|
||||||
|
|
||||||
|
$channelStats['countCon'] = count($uniqueMapConnections);
|
||||||
|
|
||||||
|
$channelsStats[] = $channelStats;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'countSub' => count($uniqueSubscriptions),
|
||||||
|
'countCon' => count($uniqueConnections),
|
||||||
|
'channels' => $channelsStats
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* compare two assoc arrays by keys. Key order is ignored
|
||||||
|
* -> if all keys from array1 exist in array2 && all keys from array2 exist in array 1, arrays are supposed to be equal
|
||||||
|
* @param array $array1
|
||||||
|
* @param array $array2
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected function arraysEqualKeys(array $array1, array $array2) : bool {
|
||||||
|
return !array_diff_key($array1, $array2) && !array_diff_key($array2, $array1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* dispatch log writing to a LogFileHandler
|
||||||
|
* @param array $meta
|
||||||
|
* @param array $log
|
||||||
|
*/
|
||||||
|
private function handleLogData(array $meta, array $log){
|
||||||
|
$logHandler = new LogFileHandler((string)$meta['stream']);
|
||||||
|
$logHandler->write($log);
|
||||||
|
}
|
||||||
|
}
|
||||||
94
websocket/app/Data/Payload.php
Normal file
94
websocket/app/Data/Payload.php
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace Exodus4D\Socket\Data;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class Payload
|
||||||
|
* @package Exodus4D\Socket\Data
|
||||||
|
* @property string $task
|
||||||
|
* @property mixed $load
|
||||||
|
*/
|
||||||
|
class Payload implements \JsonSerializable {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* error message for missing 'task' name
|
||||||
|
*/
|
||||||
|
const ERROR_TASK_MISSING = "'task' must be a not empty string";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* task name
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $task = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* payload data
|
||||||
|
* @var mixed
|
||||||
|
*/
|
||||||
|
private $load;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* optional characterId array -> recipients
|
||||||
|
* -> e.g if multiple browser tabs are open
|
||||||
|
* @var null|array
|
||||||
|
*/
|
||||||
|
private $characterIds;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Payload constructor.
|
||||||
|
* @param string $task
|
||||||
|
* @param null $load
|
||||||
|
* @param array|null $characterIds
|
||||||
|
*/
|
||||||
|
public function __construct(string $task, $load = null, ?array $characterIds = null){
|
||||||
|
$this->setTask($task);
|
||||||
|
$this->setLoad($load);
|
||||||
|
$this->setCharacterIds($characterIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $task
|
||||||
|
*/
|
||||||
|
public function setTask(string $task){
|
||||||
|
if($task){
|
||||||
|
$this->task = $task;
|
||||||
|
}else{
|
||||||
|
throw new \InvalidArgumentException(self::ERROR_TASK_MISSING);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param null $load
|
||||||
|
*/
|
||||||
|
public function setLoad($load = null){
|
||||||
|
$this->load = $load;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array|null $characterIds
|
||||||
|
*/
|
||||||
|
public function setCharacterIds(?array $characterIds){
|
||||||
|
if(is_array($characterIds)){
|
||||||
|
$this->characterIds = $characterIds;
|
||||||
|
}else{
|
||||||
|
$this->characterIds = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $name
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function __get($name){
|
||||||
|
return $this->$name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array|mixed
|
||||||
|
*/
|
||||||
|
public function jsonSerialize(){
|
||||||
|
return get_object_vars($this);
|
||||||
|
}
|
||||||
|
}
|
||||||
90
websocket/app/Log/ShellColors.php
Normal file
90
websocket/app/Log/ShellColors.php
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace Exodus4D\Socket\Log;
|
||||||
|
|
||||||
|
|
||||||
|
class ShellColors {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* all foreground color codes
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private $foregroundColors = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* all background color codes
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private $backgroundColors = [];
|
||||||
|
|
||||||
|
public function __construct() {
|
||||||
|
// set up "Shell" colors
|
||||||
|
$this->foregroundColors['black'] = '0;30';
|
||||||
|
$this->foregroundColors['dark_gray'] = '1;30';
|
||||||
|
$this->foregroundColors['blue'] = '0;34';
|
||||||
|
$this->foregroundColors['light_blue'] = '1;34';
|
||||||
|
$this->foregroundColors['green'] = '0;32';
|
||||||
|
$this->foregroundColors['light_green'] = '1;32';
|
||||||
|
$this->foregroundColors['cyan'] = '0;36';
|
||||||
|
$this->foregroundColors['light_cyan'] = '1;36';
|
||||||
|
$this->foregroundColors['red'] = '0;31';
|
||||||
|
$this->foregroundColors['light_red'] = '1;31';
|
||||||
|
$this->foregroundColors['purple'] = '0;35';
|
||||||
|
$this->foregroundColors['light_purple'] = '1;35';
|
||||||
|
$this->foregroundColors['brown'] = '0;33';
|
||||||
|
$this->foregroundColors['yellow'] = '1;33';
|
||||||
|
$this->foregroundColors['light_gray'] = '0;37';
|
||||||
|
$this->foregroundColors['white'] = '1;37';
|
||||||
|
|
||||||
|
$this->backgroundColors['black'] = '40';
|
||||||
|
$this->backgroundColors['red'] = '41';
|
||||||
|
$this->backgroundColors['green'] = '42';
|
||||||
|
$this->backgroundColors['yellow'] = '43';
|
||||||
|
$this->backgroundColors['blue'] = '44';
|
||||||
|
$this->backgroundColors['magenta'] = '45';
|
||||||
|
$this->backgroundColors['cyan'] = '46';
|
||||||
|
$this->backgroundColors['light_gray'] = '47';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get colored string
|
||||||
|
* @param string $string
|
||||||
|
* @param string|null $foregroundColor
|
||||||
|
* @param string|null $backgroundColor
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getColoredString(string $string, ?string $foregroundColor = null, ?string $backgroundColor = null) : string {
|
||||||
|
$coloredString = "";
|
||||||
|
|
||||||
|
// Check if given foreground color found
|
||||||
|
if (isset($this->foregroundColors[$foregroundColor])) {
|
||||||
|
$coloredString .= "\033[" . $this->foregroundColors[$foregroundColor] . "m";
|
||||||
|
}
|
||||||
|
// Check if given background color found
|
||||||
|
if (isset($this->backgroundColors[$backgroundColor])) {
|
||||||
|
$coloredString .= "\033[" . $this->backgroundColors[$backgroundColor] . "m";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add string and end coloring
|
||||||
|
$coloredString .= $string . "\033[0m";
|
||||||
|
|
||||||
|
return $coloredString;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns all foreground color names
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getForegroundColors() : array {
|
||||||
|
return array_keys($this->foregroundColors);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns all background color names
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getBackgroundColors() : array {
|
||||||
|
return array_keys($this->backgroundColors);
|
||||||
|
}
|
||||||
|
}
|
||||||
220
websocket/app/Log/Store.php
Normal file
220
websocket/app/Log/Store.php
Normal file
|
|
@ -0,0 +1,220 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace Exodus4D\Socket\Log;
|
||||||
|
|
||||||
|
|
||||||
|
class Store {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* default for: unique store name
|
||||||
|
*/
|
||||||
|
const DEFAULT_NAME = 'store';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* default for: echo log data in terminal
|
||||||
|
*/
|
||||||
|
const DEFAULT_LOG_TO_STDOUT = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* default for: max cached log entries
|
||||||
|
*/
|
||||||
|
const DEFAULT_LOG_STORE_SIZE = 50;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see Store::DEFAULT_NAME
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $name = self::DEFAULT_NAME;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* log store for log entries
|
||||||
|
* -> store size should be limited for memory reasons
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
private $store = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* all valid types for custom log events
|
||||||
|
* if value is false, logs for this type are ignored
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $logTypes = [
|
||||||
|
'error' => true,
|
||||||
|
'info' => true,
|
||||||
|
'debug' => true
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* if Store is locked, current state can not be changed
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
protected $locked = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var ShellColors
|
||||||
|
*/
|
||||||
|
static $colors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store constructor.
|
||||||
|
* @param string $name
|
||||||
|
*/
|
||||||
|
public function __construct(string $name){
|
||||||
|
$this->name = $name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get all stored log entries
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function getStore() : array {
|
||||||
|
return $this->store;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param bool $locked
|
||||||
|
*/
|
||||||
|
public function setLocked(bool $locked){
|
||||||
|
$this->locked = $locked;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isLocked() : bool {
|
||||||
|
return $this->locked;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $logLevel
|
||||||
|
*/
|
||||||
|
public function setLogLevel(int $logLevel){
|
||||||
|
switch($logLevel){
|
||||||
|
case 3:
|
||||||
|
$this->logTypes['error'] = true;
|
||||||
|
$this->logTypes['info'] = true;
|
||||||
|
$this->logTypes['debug'] = true;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
$this->logTypes['error'] = true;
|
||||||
|
$this->logTypes['info'] = true;
|
||||||
|
$this->logTypes['debug'] = false;
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
$this->logTypes['error'] = true;
|
||||||
|
$this->logTypes['info'] = false;
|
||||||
|
$this->logTypes['debug'] = false;
|
||||||
|
break;
|
||||||
|
case 0:
|
||||||
|
default:
|
||||||
|
$this->setLocked(true); // no logging
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* this is used for custom log events like 'error', 'debug',...
|
||||||
|
* works as dispatcher method that calls individual log*() methods
|
||||||
|
* @param $logTypes
|
||||||
|
* @param string|null $remoteAddress
|
||||||
|
* @param int|null $resourceId
|
||||||
|
* @param string $action
|
||||||
|
* @param string $message
|
||||||
|
*/
|
||||||
|
public function log($logTypes, ?string $remoteAddress, ?int $resourceId, string $action, string $message = '') : void {
|
||||||
|
if(!$this->isLocked()){
|
||||||
|
// filter out logTypes that should not be logged
|
||||||
|
$logTypes = array_filter((array)$logTypes, function(string $type) : bool {
|
||||||
|
return array_key_exists($type, $this->logTypes) && $this->logTypes[$type];
|
||||||
|
});
|
||||||
|
|
||||||
|
if($logTypes){
|
||||||
|
// get log entry data
|
||||||
|
$logData = $this->getLogData($logTypes, $remoteAddress, $resourceId, $action, $message);
|
||||||
|
|
||||||
|
if(self::DEFAULT_LOG_TO_STDOUT){
|
||||||
|
$this->echoLog($logData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add entry to local store and check size limit for store
|
||||||
|
$this->store[] = $logData;
|
||||||
|
$this->store = array_slice($this->store, self::DEFAULT_LOG_STORE_SIZE * -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get log data as array for a custom log entry
|
||||||
|
* @param array $logTypes
|
||||||
|
* @param string|null $remoteAddress
|
||||||
|
* @param int|null $resourceId
|
||||||
|
* @param string $action
|
||||||
|
* @param string $message
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function getLogData(array $logTypes, ?string $remoteAddress, ?int $resourceId, string $action, string $message = '') : array {
|
||||||
|
$file = null;
|
||||||
|
$lineNum = null;
|
||||||
|
$function = null;
|
||||||
|
|
||||||
|
$traceIndex = 4;
|
||||||
|
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, $traceIndex);
|
||||||
|
if(count($backtrace) == $traceIndex){
|
||||||
|
$caller = $backtrace[$traceIndex - 2];
|
||||||
|
$callerOrig = $backtrace[$traceIndex - 1];
|
||||||
|
|
||||||
|
$file = substr($caller['file'], strlen(dirname(dirname(dirname($caller['file'])))) + 1);
|
||||||
|
$lineNum = $caller['line'];
|
||||||
|
$function = $callerOrig['function'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$microTime = microtime(true);
|
||||||
|
$logTime = self::getDateTimeFromMicrotime($microTime);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'store' => $this->name,
|
||||||
|
'mTime' => $microTime,
|
||||||
|
'mTimeFormat1' => $logTime->format('Y-m-d H:i:s.u'),
|
||||||
|
'mTimeFormat2' => $logTime->format('H:i:s'),
|
||||||
|
'logTypes' => $logTypes,
|
||||||
|
'remoteAddress' => $remoteAddress,
|
||||||
|
'resourceId' => $resourceId,
|
||||||
|
'fileName' => $file,
|
||||||
|
'lineNumber' => $lineNum,
|
||||||
|
'function' => $function,
|
||||||
|
'action' => $action,
|
||||||
|
'message' => $message
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* echo log data to stdout -> terminal
|
||||||
|
* @param array $logData
|
||||||
|
*/
|
||||||
|
private function echoLog(array $logData) : void {
|
||||||
|
if(!self::$colors){
|
||||||
|
self::$colors = new ShellColors();
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = [
|
||||||
|
self::$colors->getColoredString($logData['mTimeFormat1'], 'dark_gray'),
|
||||||
|
self::$colors->getColoredString($logData['store'], $logData['store'] == 'webSock' ? 'brown' : 'cyan'),
|
||||||
|
$logData['remoteAddress'] . ($logData['resourceId'] ? ' #' . $logData['resourceId'] : ''),
|
||||||
|
self::$colors->getColoredString($logData['fileName'] . ' line ' . $logData['lineNumber'], 'dark_gray'),
|
||||||
|
self::$colors->getColoredString($logData['function'] . '()' . (($logData['function'] !== $logData['action']) ? ' [' . $logData['action'] . ']' : ''), 'dark_gray'),
|
||||||
|
implode(',', (array)$logData['logTypes']),
|
||||||
|
self::$colors->getColoredString($logData['message'], 'light_purple')
|
||||||
|
];
|
||||||
|
|
||||||
|
echo implode(' | ', array_filter($data)) . PHP_EOL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see https://stackoverflow.com/a/29598719/4329969
|
||||||
|
* @param float $mTime
|
||||||
|
* @return \DateTime
|
||||||
|
*/
|
||||||
|
public static function getDateTimeFromMicrotime(float $mTime) : \DateTime {
|
||||||
|
return \DateTime::createFromFormat('U.u', number_format($mTime, 6, '.', ''));
|
||||||
|
}
|
||||||
|
}
|
||||||
67
websocket/app/Socket/AbstractSocket.php
Normal file
67
websocket/app/Socket/AbstractSocket.php
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
|
||||||
|
namespace Exodus4D\Socket\Socket;
|
||||||
|
|
||||||
|
use Exodus4D\Socket\Log\Store;
|
||||||
|
use React\EventLoop;
|
||||||
|
use React\Socket;
|
||||||
|
use Ratchet\MessageComponentInterface;
|
||||||
|
|
||||||
|
abstract class AbstractSocket {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* unique name for this component
|
||||||
|
* -> should be overwritten in child instances
|
||||||
|
* -> is used as "log store" name
|
||||||
|
*/
|
||||||
|
const COMPONENT_NAME = 'default';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* global server loop
|
||||||
|
* @var EventLoop\LoopInterface
|
||||||
|
*/
|
||||||
|
protected $loop;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var MessageComponentInterface
|
||||||
|
*/
|
||||||
|
protected $handler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Store
|
||||||
|
*/
|
||||||
|
protected $logStore;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AbstractSocket constructor.
|
||||||
|
* @param EventLoop\LoopInterface $loop
|
||||||
|
* @param MessageComponentInterface $handler
|
||||||
|
* @param Store $store
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
EventLoop\LoopInterface $loop,
|
||||||
|
MessageComponentInterface $handler,
|
||||||
|
Store $store
|
||||||
|
){
|
||||||
|
$this->loop = $loop;
|
||||||
|
$this->handler = $handler;
|
||||||
|
$this->logStore = $store;
|
||||||
|
|
||||||
|
$this->log(['debug', 'info'], null, 'START', 'start Socket server…');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $logTypes
|
||||||
|
* @param Socket\ConnectionInterface|null $connection
|
||||||
|
* @param string $action
|
||||||
|
* @param string $message
|
||||||
|
*/
|
||||||
|
public function log($logTypes, ?Socket\ConnectionInterface $connection, string $action, string $message = '') : void {
|
||||||
|
if(!$this->logStore->isLocked()){
|
||||||
|
$remoteAddress = $connection ? $connection->getRemoteAddress() : null;
|
||||||
|
$this->logStore->log($logTypes, $remoteAddress, null, $action, $message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
551
websocket/app/Socket/TcpSocket.php
Normal file
551
websocket/app/Socket/TcpSocket.php
Normal file
|
|
@ -0,0 +1,551 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Created by PhpStorm.
|
||||||
|
* User: Exodus 4D
|
||||||
|
* Date: 15.02.2019
|
||||||
|
* Time: 14:29
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Exodus4D\Socket\Socket;
|
||||||
|
|
||||||
|
|
||||||
|
use Exodus4D\Socket\Log\Store;
|
||||||
|
use React\EventLoop;
|
||||||
|
use React\Socket;
|
||||||
|
use React\Promise;
|
||||||
|
use React\Stream;
|
||||||
|
use Clue\React\NDJson;
|
||||||
|
use Ratchet\MessageComponentInterface;
|
||||||
|
|
||||||
|
class TcpSocket extends AbstractSocket{
|
||||||
|
|
||||||
|
/**
|
||||||
|
* unique name for this component
|
||||||
|
* -> should be overwritten in child instances
|
||||||
|
* -> is used as "log store" name
|
||||||
|
*/
|
||||||
|
const COMPONENT_NAME = 'tcpSock';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* error message for unknown acceptType
|
||||||
|
* @see TcpSocket::DEFAULT_ACCEPT_TYPE
|
||||||
|
*/
|
||||||
|
const ERROR_ACCEPT_TYPE = "Unknown acceptType: '%s'";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* error message for connected stream is not readable
|
||||||
|
*/
|
||||||
|
const ERROR_STREAM_NOT_READABLE = "Stream is not readable. Remote address: '%s'";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* error message for connection stream is not writable
|
||||||
|
*/
|
||||||
|
const ERROR_STREAM_NOT_WRITABLE = "Stream is not writable. Remote address: '%s'";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* error message for missing 'task' key in payload
|
||||||
|
*/
|
||||||
|
const ERROR_TASK_MISSING = "Missing 'task' in payload";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* error message for unknown 'task' key in payload
|
||||||
|
*/
|
||||||
|
const ERROR_TASK_UNKNOWN = "Unknown 'task': '%s' in payload";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* error message for missing method
|
||||||
|
*/
|
||||||
|
const ERROR_METHOD_MISSING = "Method '%S' not found";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* error message for waitTimeout exceeds
|
||||||
|
* @see TcpSocket::DEFAULT_WAIT_TIMEOUT
|
||||||
|
*/
|
||||||
|
const ERROR_WAIT_TIMEOUT = "Exceeds 'waitTimeout': %ss";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* default for: accepted data type
|
||||||
|
* -> affects en/decoding socket data
|
||||||
|
*/
|
||||||
|
const DEFAULT_ACCEPT_TYPE = 'json';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* default for: wait timeout
|
||||||
|
* -> timeout until connection gets closed
|
||||||
|
* timeout should be "reset" right after successful response send to client
|
||||||
|
*/
|
||||||
|
const DEFAULT_WAIT_TIMEOUT = 3.0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* default for: send response by end() method (rather than write())
|
||||||
|
* -> connection will get closed right after successful response send to client
|
||||||
|
*/
|
||||||
|
const DEFAULT_END_WITH_RESPONSE = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* default for: add socket statistic to response payload
|
||||||
|
*/
|
||||||
|
const DEFAULT_ADD_STATS = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* max length for JSON data string
|
||||||
|
* -> throw OverflowException on exceed
|
||||||
|
*/
|
||||||
|
const JSON_DECODE_MAX_LENGTH = 65536 * 4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see TcpSocket::DEFAULT_ACCEPT_TYPE
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
private $acceptType = self::DEFAULT_ACCEPT_TYPE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see TcpSocket::DEFAULT_WAIT_TIMEOUT
|
||||||
|
* @var float
|
||||||
|
*/
|
||||||
|
private $waitTimeout = self::DEFAULT_WAIT_TIMEOUT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see TcpSocket::DEFAULT_END_WITH_RESPONSE
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
private $endWithResponse = self::DEFAULT_END_WITH_RESPONSE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see TcpSocket::DEFAULT_STATS
|
||||||
|
* @var bool
|
||||||
|
*/
|
||||||
|
private $addStats = self::DEFAULT_ADD_STATS;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* storage for all active connections
|
||||||
|
* -> can be used to get current count of connected clients
|
||||||
|
* @var \SplObjectStorage
|
||||||
|
*/
|
||||||
|
private $connections;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* max count of concurrent open connections
|
||||||
|
* -> represents number of active connected clients
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private $maxConnections = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* timestamp on startup
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
private $startupTime = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TcpSocket constructor.
|
||||||
|
* @param EventLoop\LoopInterface $loop
|
||||||
|
* @param MessageComponentInterface $handler
|
||||||
|
* @param Store $store
|
||||||
|
* @param string $acceptType
|
||||||
|
* @param float $waitTimeout
|
||||||
|
* @param bool $endWithResponse
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
EventLoop\LoopInterface $loop,
|
||||||
|
MessageComponentInterface $handler,
|
||||||
|
Store $store,
|
||||||
|
string $acceptType = self::DEFAULT_ACCEPT_TYPE,
|
||||||
|
float $waitTimeout = self::DEFAULT_WAIT_TIMEOUT,
|
||||||
|
bool $endWithResponse = self::DEFAULT_END_WITH_RESPONSE
|
||||||
|
){
|
||||||
|
parent::__construct($loop, $handler, $store);
|
||||||
|
|
||||||
|
$this->acceptType = $acceptType;
|
||||||
|
$this->waitTimeout = $waitTimeout;
|
||||||
|
$this->endWithResponse = $endWithResponse;
|
||||||
|
$this->connections = new \SplObjectStorage();
|
||||||
|
$this->startupTime = time();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Socket\ConnectionInterface $connection
|
||||||
|
*/
|
||||||
|
public function onConnect(Socket\ConnectionInterface $connection){
|
||||||
|
$this->log('debug', $connection, __FUNCTION__, 'open connection…');
|
||||||
|
|
||||||
|
if($this->isValidConnection($connection)){
|
||||||
|
// connection can be used
|
||||||
|
// add connection to global connection pool
|
||||||
|
$this->addConnection($connection);
|
||||||
|
// set waitTimeout timer for connection
|
||||||
|
$this->setTimerTimeout($connection, $this->waitTimeout);
|
||||||
|
|
||||||
|
// register connection events ... -------------------------------------------------------------------------
|
||||||
|
$this->initRead($connection)
|
||||||
|
->then($this->initDispatch($connection))
|
||||||
|
->then($this->initResponse($connection))
|
||||||
|
->then(
|
||||||
|
function(array $payload) use ($connection) {
|
||||||
|
$this->log(['debug', 'info'], $connection,'DONE', 'task "' . $payload['task'] . '" done → response send');
|
||||||
|
},
|
||||||
|
function(\Exception $e) use ($connection) {
|
||||||
|
$this->log(['debug', 'error'], $connection, 'ERROR', $e->getMessage());
|
||||||
|
$this->connectionError($connection, $e);
|
||||||
|
});
|
||||||
|
|
||||||
|
$connection->on('end', function() use ($connection) {
|
||||||
|
$this->log('debug', $connection, 'onEnd');
|
||||||
|
});
|
||||||
|
|
||||||
|
$connection->on('close', function() use ($connection) {
|
||||||
|
$this->log(['debug'], $connection, 'onClose', 'close connection');
|
||||||
|
$this->removeConnection($connection);
|
||||||
|
});
|
||||||
|
|
||||||
|
$connection->on('error', function(\Exception $e) use ($connection) {
|
||||||
|
$this->log(['debug', 'error'], $connection, 'onError', $e->getMessage());
|
||||||
|
});
|
||||||
|
}else{
|
||||||
|
// invalid connection -> can not be used
|
||||||
|
$connection->close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Socket\ConnectionInterface $connection
|
||||||
|
* @return Promise\PromiseInterface
|
||||||
|
*/
|
||||||
|
protected function initRead(Socket\ConnectionInterface $connection) : Promise\PromiseInterface {
|
||||||
|
if($connection->isReadable()){
|
||||||
|
if('json' == $this->acceptType){
|
||||||
|
// new empty stream for processing JSON
|
||||||
|
$stream = new Stream\ThroughStream();
|
||||||
|
$streamDecoded = new NDJson\Decoder($stream, true, 512, 0, self::JSON_DECODE_MAX_LENGTH);
|
||||||
|
|
||||||
|
// promise get resolved on first emit('data')
|
||||||
|
$promise = Promise\Stream\first($streamDecoded);
|
||||||
|
|
||||||
|
// register on('data') for main input stream
|
||||||
|
$connection->on('data', function ($chunk) use ($stream) {
|
||||||
|
// send current data chunk to processing stream -> resolves promise
|
||||||
|
$stream->emit('data', [$chunk]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return $promise;
|
||||||
|
}else{
|
||||||
|
return new Promise\RejectedPromise(
|
||||||
|
new \InvalidArgumentException(
|
||||||
|
sprintf(self::ERROR_ACCEPT_TYPE, $this->acceptType)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
return new Promise\RejectedPromise(
|
||||||
|
new \Exception(
|
||||||
|
sprintf(self::ERROR_STREAM_NOT_READABLE, $connection->getRemoteAddress())
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* init dispatcher for payload
|
||||||
|
* @param Socket\ConnectionInterface $connection
|
||||||
|
* @return callable
|
||||||
|
*/
|
||||||
|
protected function initDispatch(Socket\ConnectionInterface $connection) : callable {
|
||||||
|
return function(array $payload) use ($connection) : Promise\PromiseInterface {
|
||||||
|
$task = (string)$payload['task'];
|
||||||
|
if(!empty($task)){
|
||||||
|
$load = $payload['load'];
|
||||||
|
$deferred = new Promise\Deferred();
|
||||||
|
$this->dispatch($connection, $deferred, $task, $load);
|
||||||
|
return $deferred->promise();
|
||||||
|
}else{
|
||||||
|
return new Promise\RejectedPromise(
|
||||||
|
new \InvalidArgumentException(self::ERROR_TASK_MISSING)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Socket\ConnectionInterface $connection
|
||||||
|
* @param Promise\Deferred $deferred
|
||||||
|
* @param string $task
|
||||||
|
* @param null $load
|
||||||
|
*/
|
||||||
|
protected function dispatch(Socket\ConnectionInterface $connection, Promise\Deferred $deferred, string $task, $load = null) : void {
|
||||||
|
$addStatusData = false;
|
||||||
|
|
||||||
|
switch($task){
|
||||||
|
case 'getStats':
|
||||||
|
$addStatusData = true;
|
||||||
|
$deferred->resolve($this->newPayload($task, null, $addStatusData));
|
||||||
|
break;
|
||||||
|
case 'healthCheck':
|
||||||
|
$addStatusData = true;
|
||||||
|
case 'characterUpdate':
|
||||||
|
case 'characterLogout':
|
||||||
|
case 'mapConnectionAccess':
|
||||||
|
case 'mapAccess':
|
||||||
|
case 'mapUpdate':
|
||||||
|
case 'mapDeleted':
|
||||||
|
case 'logData':
|
||||||
|
if(method_exists($this->handler, 'receiveData')){
|
||||||
|
$this->log(['info'], $connection, __FUNCTION__, 'task "' . $task . '" processing…');
|
||||||
|
|
||||||
|
$deferred->resolve(
|
||||||
|
$this->newPayload(
|
||||||
|
$task,
|
||||||
|
call_user_func_array([$this->handler, 'receiveData'], [$task, $load]),
|
||||||
|
$addStatusData
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}else{
|
||||||
|
$deferred->reject(new \Exception(sprintf(self::ERROR_METHOD_MISSING, 'receiveData')));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$deferred->reject(new \InvalidArgumentException(sprintf(self::ERROR_TASK_UNKNOWN, $task)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Socket\ConnectionInterface $connection
|
||||||
|
* @return callable
|
||||||
|
*/
|
||||||
|
protected function initResponse(Socket\ConnectionInterface $connection) : callable {
|
||||||
|
return function(array $payload) use ($connection) : Promise\PromiseInterface {
|
||||||
|
$this->log('debug', $connection, 'initResponse', 'task "' . $payload['task'] . '" → init response');
|
||||||
|
|
||||||
|
$deferred = new Promise\Deferred();
|
||||||
|
$this->write($deferred, $connection, $payload);
|
||||||
|
|
||||||
|
return $deferred->promise();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Promise\Deferred $deferred
|
||||||
|
* @param Socket\ConnectionInterface $connection
|
||||||
|
* @param array $payload
|
||||||
|
*/
|
||||||
|
protected function write(Promise\Deferred $deferred, Socket\ConnectionInterface $connection, array $payload) : void {
|
||||||
|
$write = false;
|
||||||
|
if($connection->isWritable()){
|
||||||
|
if('json' == $this->acceptType){
|
||||||
|
$connection = new NDJson\Encoder($connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
// write a new chunk of data to connection stream
|
||||||
|
$write = $connection->write($payload);
|
||||||
|
|
||||||
|
if($this->endWithResponse){
|
||||||
|
// connection should be closed (and removed from this socket server)
|
||||||
|
$connection->end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if($write){
|
||||||
|
$deferred->resolve($payload);
|
||||||
|
}else{
|
||||||
|
$deferred->reject(new \Exception(
|
||||||
|
sprintf(self::ERROR_STREAM_NOT_WRITABLE, $connection->getRemoteAddress())
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* $connection has error
|
||||||
|
* -> if writable -> end() connection with $payload (close() is called by default)
|
||||||
|
* -> if readable -> close() connection
|
||||||
|
* @param Socket\ConnectionInterface $connection
|
||||||
|
* @param \Exception $e
|
||||||
|
*/
|
||||||
|
protected function connectionError(Socket\ConnectionInterface $connection, \Exception $e){
|
||||||
|
$errorMessage = $e->getMessage();
|
||||||
|
$this->log(['debug', 'error'], $connection, __FUNCTION__, $errorMessage);
|
||||||
|
|
||||||
|
if($connection->isWritable()){
|
||||||
|
if('json' == $this->acceptType){
|
||||||
|
$connection = new NDJson\Encoder($connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
// send "end" data, then close
|
||||||
|
$connection->end($this->newPayload('error', $errorMessage, true));
|
||||||
|
}else{
|
||||||
|
// close connection
|
||||||
|
$connection->close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* check if $connection is found in global pool
|
||||||
|
* @param Socket\ConnectionInterface $connection
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected function hasConnection(Socket\ConnectionInterface $connection) : bool {
|
||||||
|
return $this->connections->contains($connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* cancels a previously set timer callback for a $connection
|
||||||
|
* @param Socket\ConnectionInterface $connection
|
||||||
|
* @param string $timerName
|
||||||
|
*/
|
||||||
|
protected function cancelTimer(Socket\ConnectionInterface $connection, string $timerName){
|
||||||
|
if(
|
||||||
|
$this->hasConnection($connection) &&
|
||||||
|
($data = (array)$this->connections->offsetGet($connection)) &&
|
||||||
|
isset($data['timers']) && isset($data['timers'][$timerName]) &&
|
||||||
|
($data['timers'][$timerName] instanceof EventLoop\TimerInterface)
|
||||||
|
){
|
||||||
|
$this->loop->cancelTimer($data['timers'][$timerName]);
|
||||||
|
|
||||||
|
unset($data['timers'][$timerName]);
|
||||||
|
$this->connections->offsetSet($connection, $data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* cancels all previously set timers for a $connection
|
||||||
|
* @param Socket\ConnectionInterface $connection
|
||||||
|
*/
|
||||||
|
protected function cancelTimers(Socket\ConnectionInterface $connection){
|
||||||
|
if(
|
||||||
|
$this->hasConnection($connection) &&
|
||||||
|
($data = (array)$this->connections->offsetGet($connection)) &&
|
||||||
|
isset($data['timers'])
|
||||||
|
){
|
||||||
|
foreach((array)$data['timers'] as $timerName => $timer){
|
||||||
|
$this->loop->cancelTimer($timer);
|
||||||
|
}
|
||||||
|
|
||||||
|
$data['timers'] = [];
|
||||||
|
$this->connections->offsetSet($connection, $data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Socket\ConnectionInterface $connection
|
||||||
|
* @param string $timerName
|
||||||
|
* @param float $interval
|
||||||
|
* @param callable $timerCallback
|
||||||
|
*/
|
||||||
|
protected function setTimer(Socket\ConnectionInterface $connection, string $timerName, float $interval, callable $timerCallback){
|
||||||
|
if(
|
||||||
|
$this->hasConnection($connection) &&
|
||||||
|
($data = (array)$this->connections->offsetGet($connection)) &&
|
||||||
|
isset($data['timers'])
|
||||||
|
){
|
||||||
|
$data['timers'][$timerName] = $this->loop->addTimer($interval, function() use ($connection, $timerCallback) {
|
||||||
|
$timerCallback($connection);
|
||||||
|
});
|
||||||
|
|
||||||
|
// store new timer to $connection
|
||||||
|
$this->connections->offsetSet($connection, $data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* cancels and removes previous connection timeout timers
|
||||||
|
* -> set new connection timeout
|
||||||
|
* @param Socket\ConnectionInterface $connection
|
||||||
|
* @param float $waitTimeout
|
||||||
|
*/
|
||||||
|
protected function setTimerTimeout(Socket\ConnectionInterface $connection, float $waitTimeout = self::DEFAULT_WAIT_TIMEOUT){
|
||||||
|
$this->cancelTimer($connection, 'disconnectTimer');
|
||||||
|
$this->setTimer($connection, 'disconnectTimer', $waitTimeout, function(Socket\ConnectionInterface $connection) use ($waitTimeout) {
|
||||||
|
$errorMessage = sprintf(self::ERROR_WAIT_TIMEOUT, $waitTimeout);
|
||||||
|
|
||||||
|
$this->connectionError(
|
||||||
|
$connection,
|
||||||
|
new Promise\Timer\TimeoutException($waitTimeout, $errorMessage)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* add new connection to global pool
|
||||||
|
* @param Socket\ConnectionInterface $connection
|
||||||
|
*/
|
||||||
|
protected function addConnection(Socket\ConnectionInterface $connection){
|
||||||
|
if(!$this->hasConnection($connection)){
|
||||||
|
$this->connections->attach($connection, [
|
||||||
|
'remoteAddress' => $connection->getRemoteAddress(),
|
||||||
|
'timers' => []
|
||||||
|
]);
|
||||||
|
|
||||||
|
// update maxConnections count
|
||||||
|
$this->maxConnections = max($this->connections->count(), $this->maxConnections);
|
||||||
|
|
||||||
|
$this->log(['debug'], $connection, __FUNCTION__, 'add new connection');
|
||||||
|
}else{
|
||||||
|
$this->log(['debug'], $connection, __FUNCTION__, 'connection already exists');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* remove $connection from global connection pool
|
||||||
|
* @param Socket\ConnectionInterface $connection
|
||||||
|
*/
|
||||||
|
protected function removeConnection(Socket\ConnectionInterface $connection){
|
||||||
|
if($this->hasConnection($connection)){
|
||||||
|
$this->log(['debug'], $connection, __FUNCTION__, 'remove connection');
|
||||||
|
$this->cancelTimers($connection);
|
||||||
|
$this->connections->detach($connection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get new payload
|
||||||
|
* @param string $task
|
||||||
|
* @param null $load
|
||||||
|
* @param bool $addStats
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected function newPayload(string $task, $load = null, bool $addStats = false) : array {
|
||||||
|
$payload = [
|
||||||
|
'task' => $task,
|
||||||
|
'load' => $load
|
||||||
|
];
|
||||||
|
|
||||||
|
if($addStats || $this->addStats){
|
||||||
|
// add socket statistics
|
||||||
|
$payload['stats'] = $this->getStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* check if connection is "valid" and can be used for data transfer
|
||||||
|
* @param Socket\ConnectionInterface $connection
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected function isValidConnection(Socket\ConnectionInterface $connection) : bool {
|
||||||
|
return $connection->isReadable() || $connection->isWritable();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get socket server statistics
|
||||||
|
* -> e.g. connected clients count
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected function getStats() : array {
|
||||||
|
return [
|
||||||
|
'tcpSocket' => $this->getSocketStats(),
|
||||||
|
'webSocket' => $this->handler->getSocketStats()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get TcpSocket stats data
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected function getSocketStats() : array {
|
||||||
|
return [
|
||||||
|
'startup' => time() - $this->startupTime,
|
||||||
|
'connections' => $this->connections->count(),
|
||||||
|
'maxConnections' => $this->maxConnections,
|
||||||
|
'logs' => array_reverse($this->logStore->getStore())
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
113
websocket/app/WebSockets.php
Normal file
113
websocket/app/WebSockets.php
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Created by PhpStorm.
|
||||||
|
* User: Exodus
|
||||||
|
* Date: 01.11.2016
|
||||||
|
* Time: 18:21
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Exodus4D\Socket;
|
||||||
|
|
||||||
|
|
||||||
|
use Exodus4D\Socket\Log\Store;
|
||||||
|
use Exodus4D\Socket\Socket\TcpSocket;
|
||||||
|
use React\EventLoop;
|
||||||
|
use React\Socket;
|
||||||
|
use Ratchet\Server\IoServer;
|
||||||
|
use Ratchet\Http\HttpServer;
|
||||||
|
use Ratchet\WebSocket\WsServer;
|
||||||
|
|
||||||
|
class WebSockets {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $dsn;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
protected $wsListenPort;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $wsListenHost;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
protected $debug;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebSockets constructor.
|
||||||
|
* @param string $dsn
|
||||||
|
* @param int $wsListenPort
|
||||||
|
* @param string $wsListenHost
|
||||||
|
* @param int $debug
|
||||||
|
*/
|
||||||
|
function __construct(string $dsn, int $wsListenPort, string $wsListenHost, int $debug = 1){
|
||||||
|
$this->dsn = $dsn;
|
||||||
|
$this->wsListenPort = $wsListenPort;
|
||||||
|
$this->wsListenHost = $wsListenHost;
|
||||||
|
$this->debug = $debug;
|
||||||
|
|
||||||
|
$this->startMapSocket();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function startMapSocket(){
|
||||||
|
// global EventLoop
|
||||||
|
$loop = EventLoop\Factory::create();
|
||||||
|
|
||||||
|
// new Stores for logging -------------------------------------------------------------------------------------
|
||||||
|
$webSocketLogStore = new Store(Component\MapUpdate::COMPONENT_NAME);
|
||||||
|
$webSocketLogStore->setLogLevel($this->debug);
|
||||||
|
|
||||||
|
$tcpSocketLogStore = new Store(TcpSocket::COMPONENT_NAME);
|
||||||
|
$tcpSocketLogStore->setLogLevel($this->debug);
|
||||||
|
|
||||||
|
// global MessageComponent (main app) (handles all business logic) --------------------------------------------
|
||||||
|
$mapUpdate = new Component\MapUpdate($webSocketLogStore);
|
||||||
|
|
||||||
|
$loop->addPeriodicTimer(3, function(EventLoop\TimerInterface $timer) use ($mapUpdate) {
|
||||||
|
$mapUpdate->housekeeping($timer);
|
||||||
|
});
|
||||||
|
|
||||||
|
// TCP Socket -------------------------------------------------------------------------------------------------
|
||||||
|
$tcpSocket = new TcpSocket($loop, $mapUpdate, $tcpSocketLogStore);
|
||||||
|
// TCP Server (WebServer <-> TCPServer <-> TCPSocket communication)
|
||||||
|
$server = new Socket\Server($this->dsn, $loop, [
|
||||||
|
'tcp' => [
|
||||||
|
'backlog' => 20,
|
||||||
|
'so_reuseport' => true
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
|
||||||
|
$server->on('connection', function(Socket\ConnectionInterface $connection) use ($tcpSocket) {
|
||||||
|
$tcpSocket->onConnect($connection);
|
||||||
|
});
|
||||||
|
|
||||||
|
$server->on('error', function(\Exception $e) use ($tcpSocket) {
|
||||||
|
$tcpSocket->log(['debug', 'error'], null, 'onError', $e->getMessage());
|
||||||
|
});
|
||||||
|
|
||||||
|
// WebSocketServer --------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// Binding to 0.0.0.0 means remotes can connect (Web Clients)
|
||||||
|
$webSocketURI = $this->wsListenHost . ':' . $this->wsListenPort;
|
||||||
|
|
||||||
|
// Set up our WebSocket server for clients subscriptions
|
||||||
|
$webSock = new Socket\TcpServer($webSocketURI, $loop);
|
||||||
|
new IoServer(
|
||||||
|
new HttpServer(
|
||||||
|
new WsServer(
|
||||||
|
$mapUpdate
|
||||||
|
)
|
||||||
|
),
|
||||||
|
$webSock
|
||||||
|
);
|
||||||
|
|
||||||
|
$loop->run();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
91
websocket/cmd.php
Normal file
91
websocket/cmd.php
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
<?php
|
||||||
|
require 'vendor/autoload.php';
|
||||||
|
|
||||||
|
|
||||||
|
use Exodus4D\Socket;
|
||||||
|
|
||||||
|
if(PHP_SAPI === 'cli'){
|
||||||
|
// optional CLI params -> default values
|
||||||
|
// The default values should be fine for 99% of you!
|
||||||
|
$longOpts = [
|
||||||
|
'wsHost:' => '0.0.0.0', // WebSocket connection (for WebClients => Browser). '0.0.0.0' <-- any client can connect!
|
||||||
|
'wsPort:' => 8020, // ↪ default WebSocket URI: 127.0.0.1:8020. This is where Nginx must proxy WebSocket traffic to
|
||||||
|
'tcpHost:' => '127.0.0.1', // TcpSocket connection (for WebServer ⇄ WebSocket)
|
||||||
|
'tcpPort:' => 5555, // ↪ default TcpSocket URI: tcp://127.0.0.1:5555
|
||||||
|
'debug:' => 2 // Debug level [0-3] 0 = silent, 1 = errors, 2 = error + info, 3 = error + info + debug
|
||||||
|
];
|
||||||
|
|
||||||
|
// get options from CLI parameter + default values
|
||||||
|
$cliOpts = getopt('', array_keys($longOpts));
|
||||||
|
|
||||||
|
$options = [];
|
||||||
|
array_walk($longOpts, function($defaultVal, $optKey) use ($cliOpts, &$options) {
|
||||||
|
$key = trim($optKey, ':');
|
||||||
|
$val = $defaultVal;
|
||||||
|
if(array_key_exists($key, $cliOpts)){
|
||||||
|
$val = is_int($defaultVal) ? (int)$cliOpts[$key] : $cliOpts[$key] ;
|
||||||
|
}
|
||||||
|
$options[$key] = $val;
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* print current config parameters to Shell
|
||||||
|
* @param array $longOpts
|
||||||
|
* @param array $options
|
||||||
|
*/
|
||||||
|
$showHelp = function(array $longOpts, array $options){
|
||||||
|
$optKeys = array_keys($longOpts);
|
||||||
|
$colors = new Socket\Log\ShellColors();
|
||||||
|
$data = [];
|
||||||
|
|
||||||
|
// headline for CLI config parameters
|
||||||
|
$rowData = $colors->getColoredString(str_pad(' param', 12), 'white');
|
||||||
|
$rowData .= $colors->getColoredString(str_pad('value', 18, ' ', STR_PAD_LEFT), 'white');
|
||||||
|
$rowData .= $colors->getColoredString(str_pad('default', 15, ' ', STR_PAD_LEFT), 'white');
|
||||||
|
|
||||||
|
$data[] = $rowData;
|
||||||
|
$data[] = str_pad(' ', 45, '-');
|
||||||
|
|
||||||
|
$i = 0;
|
||||||
|
foreach($options as $optKey => $optVal){
|
||||||
|
$rowData = $colors->getColoredString(str_pad(' -' . $optKey, 12), 'yellow');
|
||||||
|
$rowData .= $colors->getColoredString(str_pad($optVal, 18, ' ', STR_PAD_LEFT), 'light_purple');
|
||||||
|
$rowData .= $colors->getColoredString(str_pad($longOpts[$optKeys[$i]], 15, ' ', STR_PAD_LEFT), 'dark_gray');
|
||||||
|
$data[] = $rowData;
|
||||||
|
$i++;
|
||||||
|
}
|
||||||
|
$data[] = '';
|
||||||
|
|
||||||
|
echo implode(PHP_EOL, $data) . PHP_EOL;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* set error reporting based on debug option value
|
||||||
|
* @param int $debug
|
||||||
|
*/
|
||||||
|
$setErrorReporting = function(int $debug){
|
||||||
|
switch($debug){
|
||||||
|
case 0: error_reporting(0); break; // Turn off all error reporting
|
||||||
|
case 1: error_reporting(E_ERROR); break; // Errors only
|
||||||
|
case 2: error_reporting(E_ALL & ~E_NOTICE); break; // Report all errors except E_NOTICE
|
||||||
|
default: error_reporting(E_ALL);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$setErrorReporting($options['debug']);
|
||||||
|
|
||||||
|
if($options['debug']){
|
||||||
|
// print if -debug > 0
|
||||||
|
$showHelp($longOpts, $options);
|
||||||
|
}
|
||||||
|
|
||||||
|
$dsn = 'tcp://' . $options['tcpHost'] . ':' . $options['tcpPort'];
|
||||||
|
|
||||||
|
new Socket\WebSockets($dsn, $options['wsPort'], $options['wsHost'], $options['debug']);
|
||||||
|
|
||||||
|
}else{
|
||||||
|
echo "Script need to be called by CLI!";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
27
websocket/composer.json
Normal file
27
websocket/composer.json
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"name": "exodus4d/pathfinder_websocket",
|
||||||
|
"description": "WebSocket extension for 'Pathfinder'",
|
||||||
|
"minimum-stability": "stable",
|
||||||
|
"license": "MIT",
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Mark Friedrich",
|
||||||
|
"email": "pathfinder@exodus4d.de"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Exodus4D\\Socket\\": "app/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php-64bit": ">=7.1",
|
||||||
|
"ext-json": "*",
|
||||||
|
"cboden/ratchet": "0.4.x",
|
||||||
|
"react/promise-stream": "1.2.*",
|
||||||
|
"clue/ndjson-react": "1.1.*"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"ext-event": "If installed, 'ExtEventLoop' class will get used as default event loop. Better performance. https://pecl.php.net/package/event"
|
||||||
|
}
|
||||||
|
}
|
||||||
1120
websocket/composer.lock
generated
Normal file
1120
websocket/composer.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue