Skip to content

Commit ba627b4

Browse files
authored
Introduce desktop notifications (#146)
Targets #135 Depends on Icinga/icinga-notifications#136 Depends on Icinga/icinga-notifications#215
2 parents 9faad19 + e9163b4 commit ba627b4

27 files changed

+2799
-3
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
/* Icinga Notifications Web | (c) 2024 Icinga GmbH | GPLv2 */
4+
5+
namespace Icinga\Module\Notifications\Clicommands;
6+
7+
use Icinga\Cli\Command;
8+
use Icinga\Module\Notifications\Daemon\Daemon;
9+
10+
class DaemonCommand extends Command
11+
{
12+
/**
13+
* Run the notifications daemon
14+
*
15+
* This program allows clients to subscribe to notifications and receive them in real-time on the desktop.
16+
*
17+
* USAGE:
18+
*
19+
* icingacli notifications daemon run [OPTIONS]
20+
*
21+
* OPTIONS
22+
*
23+
* --verbose Enable verbose output
24+
* --debug Enable debug output
25+
*/
26+
public function runAction(): void
27+
{
28+
Daemon::get();
29+
}
30+
}
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
<?php
2+
3+
/* Icinga Notifications Web | (c) 2024 Icinga GmbH | GPLv2 */
4+
5+
namespace Icinga\Module\Notifications\Controllers;
6+
7+
use Icinga\Application\Icinga;
8+
use ipl\Web\Compat\CompatController;
9+
use ipl\Web\Compat\ViewRenderer;
10+
use Zend_Layout;
11+
12+
class DaemonController extends CompatController
13+
{
14+
protected $requiresAuthentication = false;
15+
16+
public function init(): void
17+
{
18+
/*
19+
* Initialize the controller and disable the view renderer and layout as this controller provides no
20+
* graphical output
21+
*/
22+
23+
/** @var ViewRenderer $viewRenderer */
24+
$viewRenderer = $this->getHelper('viewRenderer');
25+
$viewRenderer->setNoRender();
26+
27+
/** @var Zend_Layout $layout */
28+
$layout = $this->getHelper('layout');
29+
$layout->disableLayout();
30+
}
31+
32+
public function scriptAction(): void
33+
{
34+
/**
35+
* we have to use `getRequest()->getParam` here instead of the usual `$this->param` as the required parameters
36+
* are not submitted by an HTTP request but injected manually {@see icinga-notifications-web/run.php}
37+
*/
38+
$fileName = $this->getRequest()->getParam('file', 'undefined');
39+
$extension = $this->getRequest()->getParam('extension', 'undefined');
40+
$mime = '';
41+
42+
switch ($extension) {
43+
case 'undefined':
44+
$this->httpNotFound(t("File extension is missing."));
45+
46+
// no return
47+
case '.js':
48+
$mime = 'application/javascript';
49+
50+
break;
51+
case '.js.map':
52+
$mime = 'application/json';
53+
54+
break;
55+
}
56+
57+
$root = Icinga::app()
58+
->getModuleManager()
59+
->getModule('notifications')
60+
->getBaseDir() . '/public/js';
61+
62+
$filePath = realpath($root . DIRECTORY_SEPARATOR . 'notifications-' . $fileName . $extension);
63+
if ($filePath === false || substr($filePath, 0, strlen($root)) !== $root) {
64+
if ($fileName === 'undefined') {
65+
$this->httpNotFound(t("No file name submitted"));
66+
}
67+
68+
$this->httpNotFound(sprintf(t("notifications-%s%s does not exist"), $fileName, $extension));
69+
} else {
70+
$fileStat = stat($filePath);
71+
72+
if ($fileStat) {
73+
$eTag = sprintf(
74+
'%x-%x-%x',
75+
$fileStat['ino'],
76+
$fileStat['size'],
77+
(float) str_pad((string) ($fileStat['mtime']), 16, '0')
78+
);
79+
80+
$this->getResponse()->setHeader(
81+
'Cache-Control',
82+
'public, max-age=1814400, stale-while-revalidate=604800',
83+
true
84+
);
85+
86+
if ($this->getRequest()->getServer('HTTP_IF_NONE_MATCH') === $eTag) {
87+
$this->getResponse()->setHttpResponseCode(304);
88+
} else {
89+
$this->getResponse()
90+
->setHeader('ETag', $eTag)
91+
->setHeader('Content-Type', $mime, true)
92+
->setHeader(
93+
'Last-Modified',
94+
gmdate('D, d M Y H:i:s', $fileStat['mtime']) . ' GMT'
95+
);
96+
$file = file_get_contents($filePath);
97+
if ($file) {
98+
$this->getResponse()->setBody($file);
99+
}
100+
}
101+
} else {
102+
$this->httpNotFound(sprintf(t("notifications-%s%s could not be read"), $fileName, $extension));
103+
}
104+
}
105+
}
106+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[Unit]
2+
Description=Icinga Notifications Background Daemon
3+
4+
[Service]
5+
Type=simple
6+
ExecStart=/usr/bin/icingacli notifications daemon run
7+
Restart=on-success
8+
9+
[Install]
10+
WantedBy=multi-user.target

configuration.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

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

5-
/** @var \Icinga\Application\Modules\Module $this */
5+
use Icinga\Application\Modules\Module;
6+
7+
/** @var Module $this */
68

79
$section = $this->menuSection(
810
N_('Notifications'),
@@ -92,3 +94,5 @@
9294
foreach ($cssFiles as $path) {
9395
$this->provideCssFile(ltrim(substr($path, strlen($cssDirectory)), DIRECTORY_SEPARATOR));
9496
}
97+
98+
$this->provideJsFile('notifications.js');

doc/06-Desktop-Notifications.md

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# Desktop Notifications
2+
3+
With Icinga Notifications, users are able to enable desktop notifications which will inform them about severity
4+
changes in incidents they are notified about.
5+
6+
> **Note**
7+
>
8+
> This feature is currently considered experimental and might not work as expected in all cases.
9+
> We will continue to improve this feature in the future. Your feedback is highly appreciated.
10+
11+
## How It Works
12+
13+
A user can enable this feature in their account preferences, in case Icinga Web is being accessed by using a secure
14+
connection. Once enabled, the web interface will establish a persistent connection to the web server which will push
15+
notifications to the user's browser. This connection is only established when the user is logged in and has the web
16+
interface open. This means that if the browser is closed, no notifications will be shown.
17+
18+
For this reason, desktop notifications are not meant to be a primary notification method. This is also the reason
19+
why they will only show up for incidents a contact is notified about by other means, e.g. email.
20+
21+
In order to link a contact to the currently logged-in user, both the contact's and the user's username must match.
22+
23+
### Supported Browsers
24+
25+
All browsers [supported by Icinga Web](https://icinga.com/docs/icinga-web/latest/doc/02-Installation/#browser-support)
26+
can be used to receive desktop notifications. Though, most mobile browsers are excluded, due to their aggressive energy
27+
saving mechanisms.
28+
29+
## Setup
30+
31+
To get this to work, a background daemon needs to be accessible by HTTP through the same location as the web
32+
interface. Each connection is long-lived as the daemon will push messages by using SSE (Server-Sent-Events)
33+
to each connected client.
34+
35+
### Configure The Daemon
36+
37+
The daemon is configured in the `config.ini` file located in the module's configuration directory. The default
38+
location is `/etc/icingaweb2/modules/notifications/config.ini`.
39+
40+
In there, add a new section with the following content:
41+
42+
```ini
43+
[daemon]
44+
host = [::] ; The IP address to listen on
45+
port = 9001 ; The port to listen on
46+
```
47+
48+
The values shown above are the default values. You can adjust them to your needs.
49+
50+
### Configure The Webserver
51+
52+
Since connection handling is performed by the background daemon itself, you need to configure your web server to
53+
proxy requests to the daemon. The following examples show how to configure Apache and Nginx. They're based on the
54+
default configuration Icinga Web ships with if you've used the `icingacli setup config webserver` command.
55+
56+
Adjust the base URL `/icingaweb2` to your needs and the IP address and the port to what you have configured in the
57+
daemon's configuration.
58+
59+
**Apache**
60+
61+
```
62+
<LocationMatch "^/icingaweb2/notifications/v(?<version>\d+)/subscribe">
63+
SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1
64+
RequestHeader set X-Icinga-Notifications-Protocol-Version %{MATCH_VERSION}e
65+
ProxyPass http://127.0.0.1:9001 connectiontimeout=30 timeout=30 flushpackets=on
66+
ProxyPassReverse http://127.0.0.1:9001
67+
</LocationMatch>
68+
```
69+
70+
**Nginx**
71+
72+
```
73+
location ~ ^/icingaweb2/notifications/v(\d+)/subscribe$ {
74+
proxy_pass http://127.0.0.1:9001;
75+
proxy_set_header Connection "";
76+
proxy_set_header X-Icinga-Notifications-Protocol-Version $1;
77+
proxy_http_version 1.1;
78+
proxy_buffering off;
79+
proxy_cache off;
80+
chunked_transfer_encoding off;
81+
}
82+
```
83+
84+
> **Note**
85+
>
86+
> Since these connections are long-lived, the default web server configuration might impose a too small limit on
87+
> the maximum number of connections. Make sure to adjust this limit to a higher value. If working correctly, the
88+
> daemon will limit the number of connections per client to 2.
89+
90+
### Enable The Daemon
91+
92+
The default `systemd` service, shipped with package installations, runs the background daemon.
93+
94+
<!-- {% if not icingaDocs %} -->
95+
96+
> **Note**
97+
>
98+
> If you haven't installed this module from packages, you have to configure this as a `systemd` service yourself by just
99+
> copying the example service definition from `/usr/share/icingaweb2/modules/notifications/config/systemd/icinga-notifications-web.service`
100+
> to `/etc/systemd/system/icinga-notifications-web.service`.
101+
<!-- {% endif %} -->
102+
103+
You can run the following command to enable and start the daemon.
104+
```
105+
systemctl enable --now icinga-notifications-web.service
106+
```

0 commit comments

Comments
 (0)