From 2aedc791320a987130cda0be262824ef8f3146ed Mon Sep 17 00:00:00 2001 From: Andrii Kovtun Date: Sat, 19 Sep 2020 10:40:10 +0300 Subject: [PATCH 01/11] change gomedigap/laravel-echo-server PR with hooks for version laravel-echo-server 1.6.2 --- README.md | 117 +++++++++++++++++++++++++++++++++++++ src/channels/channel.ts | 125 ++++++++++++++++++++++++++++++++++++++-- src/echo-server.ts | 10 +++- 3 files changed, 246 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 757a0b51..a338d64a 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,7 @@ Edit the default configuration of the server by adding options to your **laravel | `database` | `redis` | Database used to store data that should persist, like presence channel members. Options are currently `redis` and `sqlite` | | `databaseConfig` | `{}` | Configurations for the different database drivers [Example](#database) | | `devMode` | `false` | Adds additional logging for development purposes | +| `hookEndpoint` | `null` | The route that receives to the client-side event [Example](#hook-client-side-event) | | `host` | `null` | The host of the socket.io server ex.`app.dev`. `null` will accept connections on any IP-address | | `port` | `6001` | The port that the socket.io server should run on | | `protocol` | `http` | Must be either `http` or `https` | @@ -371,3 +372,119 @@ _Note: When using the socket.io client library from your running server, remembe #### µWebSockets deprecation µWebSockets has been [officially deprecated](https://www.npmjs.com/package/uws). Currently there is no support for µWebSockets in Socket.IO, but it may have the new [ClusterWS](https://www.npmjs.com/package/@clusterws/cws) support incoming. Meanwhile Laravel Echo Server will use [`ws` engine](https://www.npmjs.com/package/ws) by default until there is another option. + +## Hook client side event +There are 3 types of client-side event can be listen to. Here is the event names: +- join +- leave +- client_event + +### Hooks configuration +First, you need to configurate your `hookEndpoint`. Here is an example: + +```ini +"hookHost": "/api/hook", +``` + +You don't need to configure hook host. hook host value is getting from `authHost` + +`laravel-echo-server` will send a post request to hook endpoint when there is a client-side event coming. +You can get event information from `cookie` and `form`. + +#### Get data from cookie +`laravel-echo-server` directly use `cookie` from page. So you can add some cookie values like `user_id` to identify user. + +#### Get data from post form +There is always an attribute in post form called `channel`. You can get event payload of [Client Event](https://laravel.com/docs/5.7/broadcasting#client-events) of there is an client event, such as `whisper`. + +**Post form format** + +| Attribute | Description | Example | Default | +| :-------------------| :---------------------- | :-------------------| :---------------------| +| `event` | The event name. Options: `join`, `leave`, `client_event` | `join` | | +| `channel` | The channel name | `meeting` | | +| `payload` | Payload of client event. `joinChannel` or `leaveChannel` hook doesn't have payload | `{from: 'Alex', to: 'Bill'}` | `null` | + +### join channel hook +When users join in a channel `event` should be `join`. + +The request form example: +```ini +event = join +channel = helloworld +``` + +Route configuration example: +```php +Route::post('/hook', function(Request $request) { + if ($request->input('event') === 'join') { + $channel = $request->input('channel'); + $x_csrf_token = $request->header('X-CSRF-TOKEN'); + $cookie = $request->header('Cookie'); + // ... + } +}); +``` + +### leave channel hook +When users leave a channel `event` should be `leave`. + +> Notes that there is no X-CSRF-TOKEN in header when sending a post request for leave channel event, so you'd better not to use the route in `/routes/web.php`. + +The request form example: +```ini +event = leave +channel = helloworld +``` + +Route configuration example: +```php +use Illuminate\Http\Request; + +Route::post('/hook', function(Request $request) { + if ($request->input('event') === 'leave') { + $channel = $request->input('channel'); + $cookie = $request->header('Cookie'); + // ... + } +}); +``` + +### client event hook +When users use `whisper` to broadcast an event in a channel `event` should be `client_event`. + +> Notes that there is no X-CSRF-TOKEN in header when sending a post request for client-event event, so you'd better not to use the route in `/routes/web.php`. + +It will fire the client-event after using `whisper` to broadcast an event like this: +```javascript +Echo.private('chat') + .whisper('whisperEvent', { + from: this.username, + to: this.whisperTo + }); +``` + +The request form example +```ini +event = client_event +channel = helloworld +payload = {from:'Alex', to:'Bill'} +``` + +Route configuration example +```php +use Illuminate\Http\Request; + +Route::post('/hoot', function(Request $request) { + if ($request->input('event') === 'client_event') { + $channel = $request->input('channel'); + $user_id = $request->header('Cookie'); + $payload = $request->input('payload'); + $from = $payload['from']; + $to = $payload['to']; + // ... + } +}); +``` + +> Notes that even though we use an `Object` as payload of client event, the payload will be transformed to an `Array` in PHP. So remember to get your attribute from payload by using an `Array` method like `$payload['xxxx']` \ No newline at end of file diff --git a/src/channels/channel.ts b/src/channels/channel.ts index bc1dd6b3..db4caae4 100644 --- a/src/channels/channel.ts +++ b/src/channels/channel.ts @@ -1,3 +1,4 @@ +let request = require('request'); import { PresenceChannel } from './presence-channel'; import { PrivateChannel } from './private-channel'; import { Log } from './../log'; @@ -13,6 +14,14 @@ export class Channel { */ protected _clientEvents: string[] = ['client-*']; + /** + * Request client. + * + * @type {any} + */ + private request: any; + + /** * Private channel instance. */ @@ -29,6 +38,7 @@ export class Channel { constructor(private io, private options) { this.private = new PrivateChannel(options); this.presence = new PresenceChannel(io, options); + this.request = request; if (this.options.devMode) { Log.success('Channels are ready.'); @@ -44,7 +54,7 @@ export class Channel { this.joinPrivate(socket, data); } else { socket.join(data.channel); - this.onJoin(socket, data.channel); + this.onJoin(socket, data.channel, data.auth); } } } @@ -66,6 +76,7 @@ export class Channel { this.io.sockets.connected[socket.id] .broadcast.to(data.channel) .emit(data.event, data.channel, data.data); + this.hook(socket, data.channel, data.auth, "onClientEvent"); } } } @@ -73,7 +84,7 @@ export class Channel { /** * Leave a channel. */ - leave(socket: any, channel: string, reason: string): void { + leave(socket: any, channel: string, reason: string, auth: any): void { if (channel) { if (this.isPresence(channel)) { this.presence.leave(socket, channel) @@ -84,6 +95,8 @@ export class Channel { if (this.options.devMode) { Log.info(`[${new Date().toISOString()}] - ${socket.id} left channel: ${channel} (${reason})`); } + + this.hook(socket, channel, auth, "onLeave"); } } @@ -117,7 +130,7 @@ export class Channel { this.presence.join(socket, data.channel, member); } - this.onJoin(socket, data.channel); + this.onJoin(socket, data.channel, data.auth); }, error => { if (this.options.devMode) { Log.error(error.reason); @@ -138,10 +151,12 @@ export class Channel { /** * On join a channel log success. */ - onJoin(socket: any, channel: string): void { + onJoin(socket: any, channel: string, auth: any): void { if (this.options.devMode) { Log.info(`[${new Date().toISOString()}] - ${socket.id} joined channel: ${channel}`); } + + this.hook(socket, channel, auth, "onJoin"); } /** @@ -164,4 +179,106 @@ export class Channel { isInChannel(socket: any, channel: string): boolean { return !!socket.rooms[channel]; } + + /** + * + * @param {any} socket + * @param {string} channel + * @param {object} auth + * @param {string} hookEndpoint + * @param {string} hookName + */ + hook(socket:any, channel: any, auth: any, hookName: string) { + if (typeof this.options.hookHost == 'undefined' || + !this.options.hookHost || + typeof this.options.hooks == 'undefined' || + !this.options.hooks) { + return; + } + + let hookEndpoint = this.getHookEndpoint(hookName); + + if (hookEndpoint == null) { + return; + } + + let options = this.prepareHookHeaders(socket, auth, channel, hookEndpoint) + + this.request.post(options, (error, response, body, next) => { + if (error) { + if (this.options.devMode) { + Log.error(`[${new Date().toLocaleTimeString()}] - Error call ${hookName} hook ${socket.id} for ${options.form.channel_name}`); + } + + Log.error(error); + } else if (response.statusCode !== 200) { + if (this.options.devMode) { + Log.warning(`[${new Date().toLocaleTimeString()}] - Error call ${hookName} hook ${socket.id} for ${options.form.channel_name}`); + Log.error(response.body); + } + } else { + if (this.options.devMode) { + Log.info(`[${new Date().toLocaleTimeString()}] - Call ${hookName} hook for ${socket.id} for ${options.form.channel_name}: ${response.body}`); + } + } + }); + } + + /** + * Get hook endpoint for request to app server. + * + * @param {string} hookName + * @returns {string} + */ + getHookEndpoint(hookName: string): string { + let hookEndpoint = null; + switch(hookName) { + case "onJoin": { + if (!this.options.hooks.onJoinEndpoint) { + break; + } + hookEndpoint = this.options.hooks.onJoinEndpoint; + break; + } + case "onLeave": { + if (!this.options.hooks.onLeaveEndpoint) { + break; + } + hookEndpoint = this.options.hooks.onLeaveEndpoint; + break; + } + case "onClientEvent": { + if (!this.options.hooks.onClientEventEndpoint) { + break; + } + hookEndpoint = this.options.hooks.onClientEventEndpoint; + break; + } + default: { + Log.error('cannot find hookEndpoint for hookName: ' + hookName); + break; + } + } + return hookEndpoint; + } + + /** + * Prepare headers for request to app server. + * + * @param {any} socket + * @param {any} auth + * @param {string} channel + * @param {string} hookEndpoint + * @returns {any} + */ + prepareHookHeaders(socket: any, auth: any, channel: string, hookEndpoint: string): any { + let options = { + url: this.options.hookHost + hookEndpoint, + form: { channel_name: channel }, + headers: (auth && auth.headers) ? auth.headers : {} + }; + options.headers['Cookie'] = socket.request.headers.cookie; + options.headers['X-Requested-With'] = 'XMLHttpRequest'; + return options; + } } diff --git a/src/echo-server.ts b/src/echo-server.ts index 069335c2..035c4417 100644 --- a/src/echo-server.ts +++ b/src/echo-server.ts @@ -44,6 +44,12 @@ export class EchoServer { allowOrigin: '', allowMethods: '', allowHeaders: '' + }, + hookHost: null, + hooks: { + "onJoinEndpoint": null, + "onLeaveEndpoint": null, + "onClientEventEndpoint": null } }; @@ -225,7 +231,7 @@ export class EchoServer { */ onUnsubscribe(socket: any): void { socket.on('unsubscribe', data => { - this.channel.leave(socket, data.channel, 'unsubscribed'); + this.channel.leave(socket, data.channel, 'unsubscribed', data.auth); }); } @@ -236,7 +242,7 @@ export class EchoServer { socket.on('disconnecting', (reason) => { Object.keys(socket.rooms).forEach(room => { if (room !== socket.id) { - this.channel.leave(socket, room, reason); + this.channel.leave(socket, room, reason, {}); } }); }); From c1e46b43443fa1a6779a00d285a561bdc29a08e1 Mon Sep 17 00:00:00 2001 From: Andrii Kovtun Date: Tue, 22 Sep 2020 13:10:00 +0300 Subject: [PATCH 02/11] Create docker-entrypoint --- bin/docker-entrypoint | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 bin/docker-entrypoint diff --git a/bin/docker-entrypoint b/bin/docker-entrypoint new file mode 100644 index 00000000..f62de73a --- /dev/null +++ b/bin/docker-entrypoint @@ -0,0 +1,38 @@ +#!/bin/sh +set -e + +# laravel-echo-server init +if [[ "$1" == 'init' ]]; then + set -- laravel-echo-server "$@" +fi + +# laravel-echo-server +if [[ "$1" == 'start' ]] || [[ "$1" == 'client:add' ]] || [[ "$1" == 'client:remove' ]]; then + if [[ "${GENERATE_CONFIG:-true}" == "false" ]]; then + # wait for another process to inject the config + echo -n "Waiting for /app/laravel-echo-server.json" + while [[ ! -f /app/laravel-echo-server.json ]]; do + sleep 2 + echo -n "." + done + elif [[ ! -f /app/laravel-echo-server.json ]]; then + cp /usr/local/src/laravel-echo-server.json /app/laravel-echo-server.json + # Replace with environment variables + sed -i "s|LARAVEL_ECHO_SERVER_DB|${LARAVEL_ECHO_SERVER_DB:-redis}|i" /app/laravel-echo-server.json + sed -i "s|REDIS_HOST|${REDIS_HOST:-redis}|i" /app/laravel-echo-server.json + sed -i "s|REDIS_PORT|${REDIS_PORT:-6379}|i" /app/laravel-echo-server.json + sed -i "s|REDIS_PASSWORD|${REDIS_PASSWORD}|i" /app/laravel-echo-server.json + sed -i "s|REDIS_PREFIX|${REDIS_PREFIX:-laravel_database_}|i" /app/laravel-echo-server.json + sed -i "s|REDIS_DB|${REDIS_DB:-0}|i" /app/laravel-echo-server.json + # Remove password config if it is empty + sed -i "s|\"password\": \"\",||i" /app/laravel-echo-server.json + fi + set -- laravel-echo-server "$@" +fi + +# first arg is `-f` or `--some-option` +if [[ "${1#-}" != "$1" ]]; then + set -- laravel-echo-server "$@" +fi + +exec "$@" From cc6ebb467aff1f400da156d54e2cec67aaaf1d2f Mon Sep 17 00:00:00 2001 From: Andrii Kovtun Date: Tue, 22 Sep 2020 13:17:30 +0300 Subject: [PATCH 03/11] Create health-check --- bin/health-check | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 bin/health-check diff --git a/bin/health-check b/bin/health-check new file mode 100644 index 00000000..a8377d14 --- /dev/null +++ b/bin/health-check @@ -0,0 +1,39 @@ +#!/bin/sh +set -x + +_init () { + scheme="http://" + address="$(netstat -nplt 2>/dev/null | awk ' /(.*\/laravel-echo-serv)/ { gsub(":::","127.0.0.1:",$4); print $4}')" + resource="/socket.io/socket.io.js" + start=$(stat -c "%Y" /proc/1) +} + +fn_health_check () { + # In distributed environment like Swarm, traffic is routed + # to a container only when it reports a `healthy` status. So, we exit + # with 0 to ensure healthy status till distributed service starts (120s). + # + # Refer: https://github.com/moby/moby/pull/28938#issuecomment-301753272 + if [[ $(( $(date +%s) - start )) -lt 120 ]]; then + exit 0 + else + # Get the http response code + http_response=$(curl -s -k -o /dev/null -w "%{http_code}" ${scheme}${address}${resource}) + + # Get the http response body + http_response_body=$(curl -k -s ${scheme}${address}${resource}) + + # server returns response 403 and body "SSL required" if non-TLS + # connection is attempted on a TLS-configured server. Change + # the scheme and try again + if [[ "$http_response" = "403" ]] && [[ "$http_response_body" = "SSL required" ]]; then + scheme="https://" + http_response=$(curl -s -k -o /dev/null -w "%{http_code}" ${scheme}${address}${resource}) + fi + + # If http_response is 200 - server is up. + [[ "$http_response" = "200" ]] + fi +} + +_init && fn_health_check From 32f5eefb185da587dbb1edae24c8298c118ae601 Mon Sep 17 00:00:00 2001 From: Andrii Kovtun Date: Tue, 22 Sep 2020 13:24:39 +0300 Subject: [PATCH 04/11] Create Dockerfile --- Dockerfile | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..2bce3475 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +FROM node:12-alpine + +WORKDIR /app + +COPY . /app + +RUN npm ci && npm run prepublish + +RUN ln -s /app/bin/server.js /usr/bin/laravel-echo-server + +COPY bin/docker-entrypoint bin/health-check /usr/local/bin/ + +ENTRYPOINT ["docker-entrypoint"] + +VOLUME /app + +EXPOSE 6001 + +HEALTHCHECK --interval=30s --timeout=5s \ + CMD /usr/local/bin/health-check + +CMD ["start"] From f3de93e60fca660438e30963fdac2568477a2436 Mon Sep 17 00:00:00 2001 From: Evgeny Zhiryakov Date: Tue, 22 Sep 2020 17:41:42 +0600 Subject: [PATCH 05/11] Change files mode --- bin/docker-entrypoint | 0 bin/health-check | 0 2 files changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 bin/docker-entrypoint mode change 100644 => 100755 bin/health-check diff --git a/bin/docker-entrypoint b/bin/docker-entrypoint old mode 100644 new mode 100755 diff --git a/bin/health-check b/bin/health-check old mode 100644 new mode 100755 From d34ba91f3ec4eeb6bb15dfc9b7dd4739ed8c7587 Mon Sep 17 00:00:00 2001 From: ezhiryakov <71262048+ezhiryakov@users.noreply.github.com> Date: Wed, 23 Sep 2020 13:42:05 +0600 Subject: [PATCH 06/11] Add curl for health-check --- Dockerfile | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 2bce3475..76aa4239 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,9 @@ -FROM node:12-alpine +FROM node:lts-alpine WORKDIR /app +RUN apk add --update --no-cache curl + COPY . /app RUN npm ci && npm run prepublish From c09e738faf1cee8481a663144eef023ff4f13ca3 Mon Sep 17 00:00:00 2001 From: Evgeny Zhiryakov Date: Fri, 11 Dec 2020 20:04:04 +0600 Subject: [PATCH 07/11] Update certificates --- bin/docker-entrypoint | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bin/docker-entrypoint b/bin/docker-entrypoint index f62de73a..87312d1d 100755 --- a/bin/docker-entrypoint +++ b/bin/docker-entrypoint @@ -1,6 +1,8 @@ #!/bin/sh set -e +/usr/sbin/update-ca-certificates + # laravel-echo-server init if [[ "$1" == 'init' ]]; then set -- laravel-echo-server "$@" From 99dcf65f304a568f90e70072f87c9521c699b8de Mon Sep 17 00:00:00 2001 From: Andrii Kovtun Date: Fri, 11 Dec 2020 21:58:48 +0200 Subject: [PATCH 08/11] implement and set NODE_TLS_REJECT_UNAUTHORIZED --- README.md | 1 + src/cli/cli.ts | 3 ++- src/echo-server.ts | 3 ++- src/server.ts | 4 ++++ 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a338d64a..ca42bbb4 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,7 @@ file, the following options can be overridden: - `sslCertPath`: `LARAVEL_ECHO_SERVER_SSL_CERT` - `sslPassphrase`: `LARAVEL_ECHO_SERVER_SSL_PASS` - `sslCertChainPath`: `LARAVEL_ECHO_SERVER_SSL_CHAIN` +- `rejectUnautorized`: `NODE_TLS_REJECT_UNAUTHORIZED` ### Running with SSL diff --git a/src/cli/cli.ts b/src/cli/cli.ts index ec12f200..b18621c4 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -38,7 +38,8 @@ export class Cli { LARAVEL_ECHO_SERVER_SSL_CERT: "sslCertPath", LARAVEL_ECHO_SERVER_SSL_KEY: "sslKeyPath", LARAVEL_ECHO_SERVER_SSL_CHAIN: "sslCertChainPath", - LARAVEL_ECHO_SERVER_SSL_PASS: "sslPassphrase" + LARAVEL_ECHO_SERVER_SSL_PASS: "sslPassphrase", + NODE_TLS_REJECT_UNAUTHORIZED: "rejectUnautorized" }; /** diff --git a/src/echo-server.ts b/src/echo-server.ts index 035c4417..b522b89c 100644 --- a/src/echo-server.ts +++ b/src/echo-server.ts @@ -50,7 +50,8 @@ export class EchoServer { "onJoinEndpoint": null, "onLeaveEndpoint": null, "onClientEventEndpoint": null - } + }, + rejectUnautorized: '' }; /** diff --git a/src/server.ts b/src/server.ts index 0402fda4..35e9e0f2 100644 --- a/src/server.ts +++ b/src/server.ts @@ -106,6 +106,10 @@ export class Server { next(); }); + if (this.options.rejectUnautorized && this.options.rejectUnautorized !== '') { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = this.options.rejectUnautorized + } + if (secure) { var httpServer = https.createServer(this.options, this.express); } else { From 5a84897c2c7b15b6d4e9a59341eb5a97b9cd5eb2 Mon Sep 17 00:00:00 2001 From: Andrii Kovtun Date: Fri, 11 Dec 2020 22:10:55 +0200 Subject: [PATCH 09/11] update startup log --- src/echo-server.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/echo-server.ts b/src/echo-server.ts index b522b89c..b2457497 100644 --- a/src/echo-server.ts +++ b/src/echo-server.ts @@ -135,6 +135,8 @@ export class EchoServer { } else { Log.info('Starting server...\n') } + + Log.info(`Searching hooks...\n`); } /** From b02417942764e31a702c493fd286caaf2ad4031c Mon Sep 17 00:00:00 2001 From: Andrii Kovtun Date: Sat, 12 Dec 2020 18:14:08 +0200 Subject: [PATCH 10/11] implement onJoin/onLeave channel regExp --- src/channels/channel.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/channels/channel.ts b/src/channels/channel.ts index db4caae4..cd00d5ea 100644 --- a/src/channels/channel.ts +++ b/src/channels/channel.ts @@ -196,7 +196,7 @@ export class Channel { return; } - let hookEndpoint = this.getHookEndpoint(hookName); + let hookEndpoint = this.getHookEndpoint(hookName, channel); if (hookEndpoint == null) { return; @@ -230,13 +230,16 @@ export class Channel { * @param {string} hookName * @returns {string} */ - getHookEndpoint(hookName: string): string { + getHookEndpoint(hookName: string, channel: any): string { let hookEndpoint = null; switch(hookName) { case "onJoin": { if (!this.options.hooks.onJoinEndpoint) { break; } + if (this.options.hooks.onJoinRegexp && !(new RegExp(this.options.hooks.onJoinRegexp)).test(channel)) { + break; + } hookEndpoint = this.options.hooks.onJoinEndpoint; break; } @@ -244,6 +247,9 @@ export class Channel { if (!this.options.hooks.onLeaveEndpoint) { break; } + if (this.options.hooks.onLeaveRegexp && !(new RegExp(this.options.hooks.onLeaveRegexp)).test(channel)) { + break; + } hookEndpoint = this.options.hooks.onLeaveEndpoint; break; } From 3e2031aa5921dddaa3637530f8e68b47c3fd99b7 Mon Sep 17 00:00:00 2001 From: Evgeny Zhiryakov <71262048+ezhiryakov@users.noreply.github.com> Date: Mon, 26 Dec 2022 16:20:19 +0600 Subject: [PATCH 11/11] Update Dockerfile --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 76aa4239..9396a19e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:lts-alpine +FROM node:14.16.1-alpine WORKDIR /app