Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce desktop notifications #146

Merged
merged 26 commits into from
Jun 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9b8d6bb
feat(Model\Daemon): mappings for new database table `browser_session`
ncosta-ic May 17, 2024
7592ca2
feat: introduce daemon backend, daemon data classes
ncosta-ic May 17, 2024
8cac093
refactor(Model): type-hint contact identifier
ncosta-ic May 17, 2024
84819f3
feat: add session storage hook
ncosta-ic May 17, 2024
4b432ff
feat: add notification banner icons
ncosta-ic May 17, 2024
f8217b1
feat(JS): add service worker and JS module
ncosta-ic May 17, 2024
42cbb5b
feat(Controller): load logic and JS file routing
ncosta-ic May 17, 2024
0ba0adb
docs: add desktop notifications documentation
ncosta-ic May 17, 2024
b28343c
js: Simplify constructor, split dependency checks and initial load
nilmerg May 24, 2024
386d03d
js: Attempt to open an event stream during init
nilmerg May 24, 2024
666fbb7
IncidentHistory: Add group by workaround
nilmerg May 27, 2024
002e0bf
Daemon: Only fetch transmitted notifications
nilmerg May 27, 2024
ea0dae9
js: Expect the server to render an object's name
nilmerg May 27, 2024
92c7f59
js: Expect the server to provide a notification message
nilmerg May 27, 2024
ec3dd10
DaemonCommand: Add documentation
nilmerg May 28, 2024
535aba7
Event: Omit `id` property
nilmerg May 28, 2024
1f59884
js: Add note that new workers cannot take over active ones
nilmerg May 28, 2024
86c990e
Use a versioned route to subscribe to notification messages
nilmerg May 28, 2024
0f789dc
Introduce default systemd file for the background daemon
nilmerg May 28, 2024
fae1346
doc: Document the desktop notification feature
nilmerg May 28, 2024
fb306e9
daemon/script: Make absolutely sure to prevent traversal attacks
nilmerg May 28, 2024
5aa604c
Daemon: Let integrations render an notification's title
nilmerg May 31, 2024
c18ad41
fix(Connection): connection gets properly mapped for both IPv4 and IP…
ncosta-ic Jun 3, 2024
3be1b33
Daemon\Server: Enhance error logging
nilmerg Jun 7, 2024
0028789
refactor(SessionStorage): add username trimming
ncosta-ic Jun 10, 2024
e9163b4
feat(Session): provide `authenticated_at` programmatically
ncosta-ic Jun 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions application/clicommands/DaemonCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

/* Icinga Notifications Web | (c) 2024 Icinga GmbH | GPLv2 */

namespace Icinga\Module\Notifications\Clicommands;

use Icinga\Cli\Command;
use Icinga\Module\Notifications\Daemon\Daemon;

class DaemonCommand extends Command
{
/**
* Run the notifications daemon
*
* This program allows clients to subscribe to notifications and receive them in real-time on the desktop.
*
* USAGE:
*
* icingacli notifications daemon run [OPTIONS]
*
* OPTIONS
*
* --verbose Enable verbose output
* --debug Enable debug output
*/
public function runAction(): void
{
Daemon::get();
}
}
106 changes: 106 additions & 0 deletions application/controllers/DaemonController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?php

/* Icinga Notifications Web | (c) 2024 Icinga GmbH | GPLv2 */

namespace Icinga\Module\Notifications\Controllers;

use Icinga\Application\Icinga;
use ipl\Web\Compat\CompatController;
use ipl\Web\Compat\ViewRenderer;
use Zend_Layout;

class DaemonController extends CompatController
{
protected $requiresAuthentication = false;

public function init(): void
{
/*
* Initialize the controller and disable the view renderer and layout as this controller provides no
* graphical output
*/

/** @var ViewRenderer $viewRenderer */
$viewRenderer = $this->getHelper('viewRenderer');
$viewRenderer->setNoRender();

/** @var Zend_Layout $layout */
$layout = $this->getHelper('layout');
$layout->disableLayout();
}

public function scriptAction(): void
{
/**
* we have to use `getRequest()->getParam` here instead of the usual `$this->param` as the required parameters
* are not submitted by an HTTP request but injected manually {@see icinga-notifications-web/run.php}
*/
$fileName = $this->getRequest()->getParam('file', 'undefined');
$extension = $this->getRequest()->getParam('extension', 'undefined');
$mime = '';

switch ($extension) {
case 'undefined':
$this->httpNotFound(t("File extension is missing."));

// no return
case '.js':
$mime = 'application/javascript';

break;
case '.js.map':
$mime = 'application/json';

break;
}

$root = Icinga::app()
->getModuleManager()
->getModule('notifications')
->getBaseDir() . '/public/js';

$filePath = realpath($root . DIRECTORY_SEPARATOR . 'notifications-' . $fileName . $extension);
if ($filePath === false || substr($filePath, 0, strlen($root)) !== $root) {
if ($fileName === 'undefined') {
$this->httpNotFound(t("No file name submitted"));
}

$this->httpNotFound(sprintf(t("notifications-%s%s does not exist"), $fileName, $extension));
} else {
$fileStat = stat($filePath);

if ($fileStat) {
$eTag = sprintf(
'%x-%x-%x',
$fileStat['ino'],
$fileStat['size'],
(float) str_pad((string) ($fileStat['mtime']), 16, '0')
);

$this->getResponse()->setHeader(
'Cache-Control',
'public, max-age=1814400, stale-while-revalidate=604800',
true
);

if ($this->getRequest()->getServer('HTTP_IF_NONE_MATCH') === $eTag) {
$this->getResponse()->setHttpResponseCode(304);
} else {
$this->getResponse()
->setHeader('ETag', $eTag)
->setHeader('Content-Type', $mime, true)
->setHeader(
'Last-Modified',
gmdate('D, d M Y H:i:s', $fileStat['mtime']) . ' GMT'
);
$file = file_get_contents($filePath);
if ($file) {
$this->getResponse()->setBody($file);
}
}
} else {
$this->httpNotFound(sprintf(t("notifications-%s%s could not be read"), $fileName, $extension));
}
}
}
}
10 changes: 10 additions & 0 deletions config/systemd/icinga-notifications-web.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[Unit]
Description=Icinga Notifications Background Daemon

[Service]
Type=simple
ExecStart=/usr/bin/icingacli notifications daemon run
Restart=on-success

[Install]
WantedBy=multi-user.target
6 changes: 5 additions & 1 deletion configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

/* Icinga Notifications Web | (c) 2023 Icinga GmbH | GPLv2 */

/** @var \Icinga\Application\Modules\Module $this */
use Icinga\Application\Modules\Module;

/** @var Module $this */

$section = $this->menuSection(
N_('Notifications'),
Expand Down Expand Up @@ -92,3 +94,5 @@
foreach ($cssFiles as $path) {
$this->provideCssFile(ltrim(substr($path, strlen($cssDirectory)), DIRECTORY_SEPARATOR));
}

$this->provideJsFile('notifications.js');
106 changes: 106 additions & 0 deletions doc/06-Desktop-Notifications.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# Desktop Notifications

With Icinga Notifications, users are able to enable desktop notifications which will inform them about severity
changes in incidents they are notified about.

> **Note**
>
> This feature is currently considered experimental and might not work as expected in all cases.
> We will continue to improve this feature in the future. Your feedback is highly appreciated.

## How It Works

A user can enable this feature in their account preferences, in case Icinga Web is being accessed by using a secure
connection. Once enabled, the web interface will establish a persistent connection to the web server which will push
notifications to the user's browser. This connection is only established when the user is logged in and has the web
interface open. This means that if the browser is closed, no notifications will be shown.

For this reason, desktop notifications are not meant to be a primary notification method. This is also the reason
why they will only show up for incidents a contact is notified about by other means, e.g. email.

In order to link a contact to the currently logged-in user, both the contact's and the user's username must match.

### Supported Browsers

All browsers [supported by Icinga Web](https://icinga.com/docs/icinga-web/latest/doc/02-Installation/#browser-support)
can be used to receive desktop notifications. Though, most mobile browsers are excluded, due to their aggressive energy
saving mechanisms.

## Setup

To get this to work, a background daemon needs to be accessible by HTTP through the same location as the web
interface. Each connection is long-lived as the daemon will push messages by using SSE (Server-Sent-Events)
to each connected client.

### Configure The Daemon

The daemon is configured in the `config.ini` file located in the module's configuration directory. The default
location is `/etc/icingaweb2/modules/notifications/config.ini`.

In there, add a new section with the following content:

```ini
[daemon]
host = [::] ; The IP address to listen on
port = 9001 ; The port to listen on
```

The values shown above are the default values. You can adjust them to your needs.

### Configure The Webserver

Since connection handling is performed by the background daemon itself, you need to configure your web server to
proxy requests to the daemon. The following examples show how to configure Apache and Nginx. They're based on the
default configuration Icinga Web ships with if you've used the `icingacli setup config webserver` command.

Adjust the base URL `/icingaweb2` to your needs and the IP address and the port to what you have configured in the
daemon's configuration.

**Apache**

```
<LocationMatch "^/icingaweb2/notifications/v(?<version>\d+)/subscribe">
SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
RequestHeader set X-Icinga-Notifications-Protocol-Version %{MATCH_VERSION}e
ProxyPass http://127.0.0.1:9001 connectiontimeout=30 timeout=30 flushpackets=on
ProxyPassReverse http://127.0.0.1:9001
</LocationMatch>
```

**Nginx**

```
location ~ ^/icingaweb2/notifications/v(\d+)/subscribe$ {
proxy_pass http://127.0.0.1:9001;
proxy_set_header Connection "";
proxy_set_header X-Icinga-Notifications-Protocol-Version $1;
proxy_http_version 1.1;
proxy_buffering off;
proxy_cache off;
chunked_transfer_encoding off;
}
```

> **Note**
>
> Since these connections are long-lived, the default web server configuration might impose a too small limit on
> the maximum number of connections. Make sure to adjust this limit to a higher value. If working correctly, the
> daemon will limit the number of connections per client to 2.

### Enable The Daemon

The default `systemd` service, shipped with package installations, runs the background daemon.

<!-- {% if not icingaDocs %} -->

> **Note**
>
> If you haven't installed this module from packages, you have to configure this as a `systemd` service yourself by just
> copying the example service definition from `/usr/share/icingaweb2/modules/notifications/config/systemd/icinga-notifications-web.service`
> to `/etc/systemd/system/icinga-notifications-web.service`.
<!-- {% endif %} -->

You can run the following command to enable and start the daemon.
```
systemctl enable --now icinga-notifications-web.service
```
Loading
Loading