Project

General

Profile

Download (18.8 KB) Statistics
| Branch: | Tag: | Revision:
1
<?php
2
/*
3
 * notices.inc
4
 *
5
 * part of pfSense (https://www.pfsense.org)
6
 * Copyright (c) 2005 Colin Smith (ethethlay@gmail.com)
7
 * Copyright (c) 2005-2013 BSD Perimeter
8
 * Copyright (c) 2013-2016 Electric Sheep Fencing
9
 * Copyright (c) 2014-2024 Rubicon Communications, LLC (Netgate)
10
 * All rights reserved.
11
 *
12
 * Licensed under the Apache License, Version 2.0 (the "License");
13
 * you may not use this file except in compliance with the License.
14
 * You may obtain a copy of the License at
15
 *
16
 * http://www.apache.org/licenses/LICENSE-2.0
17
 *
18
 * Unless required by applicable law or agreed to in writing, software
19
 * distributed under the License is distributed on an "AS IS" BASIS,
20
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21
 * See the License for the specific language governing permissions and
22
 * limitations under the License.
23
 */
24

    
25
require_once("globals.inc");
26
require_once("functions.inc");
27
require_once("led.inc");
28

    
29
global $notice_root, $notice_path, $notice_queue, $notice_last;
30
$notice_root  = g_get('tmp_path') . '/notices';
31
$notice_path  = $notice_root . '/notices.serial';
32
$notice_queue = $notice_root . '/smtp.queue';
33
$notice_last  = $notice_root . '/smtp.last';
34

    
35
global $smtp_authentication_mechanisms;
36
$smtp_authentication_mechanisms = array(
37
	'PLAIN' => 'PLAIN',
38
	'LOGIN' => 'LOGIN');
39
/* Other SMTP Authentication Mechanisms that could be supported.
40
 * Note that MD5 is no longer considered secure.
41
 *	'GSSAPI' => 'GSSAPI ' . gettext("Generic Security Services Application Program Interface")
42
 *	'DIGEST-MD5' => 'DIGEST-MD5 ' . gettext("Digest access authentication")
43
 *	'MD5' => 'MD5'
44
 *	'CRAM-MD5' => 'CRAM-MD5'
45
*/
46
global $pushover_sounds;
47
$pushover_sounds = array(
48
	'devicedefault' => 'Device Default',
49
	'pushover' => 'Pushover',
50
	'bike' => 'Bike',
51
	'bugle' => 'Bugle',
52
	'cashregister' => 'Cash Register',
53
	'classical' => 'Classical',
54
	'cosmic' => 'Cosmic',
55
	'falling' => 'Falling',
56
	'gamelan' => 'Gamelan',
57
	'incoming' => 'Incoming',
58
	'intermission' => 'Intermission',
59
	'magic' => 'Magic',
60
	'mechanical' => 'Mechanical',
61
	'pianobar' => 'Piano Bar',
62
	'siren' => 'Siren',
63
	'spacealarm' => 'Space Alarm',
64
	'tugboat' => 'Tug Boat',
65
	'alien' => 'Alien (long)',
66
	'climb' => 'Climb (long)',
67
	'persistent' => 'Persistent (long)',
68
	'echo' => 'Echo (long)',
69
	'updown' => 'Up Down (long)',
70
	'vibrate' => 'Vibrate only',
71
	'none' => 'None (silent)');
72

    
73
/****f* notices/notices_setup
74
 * NAME
75
 *   notices_setup
76
 * INPUTS
77
 *   None
78
 * RESULT
79
 *   Sets up notice queues in such a way that any local daemon/user can
80
 *   submit notification messages.
81
 ******/
82
function notices_setup() {
83
	global $notice_root, $notice_path, $notice_queue, $notice_last;
84

    
85
	/* Setup notice queue directory */
86
	if (!is_dir($notice_root) || is_link($notice_root)) {
87
		@unlink_if_exists($notice_root);
88
		safe_mkdir($notice_root, 0755);
89
	}
90

    
91
	/* Create notice queue files */
92
	foreach ([$notice_path, $notice_queue, $notice_last] as $nf) {
93
		if (!is_file($nf) || is_link($nf)) {
94
			@unlink_if_exists($nf);
95
			touch($nf);
96
			chmod($nf, 0777);
97
		}
98
	}
99
}
100

    
101
/****f* notices/file_notice
102
 * NAME
103
 *   file_notice
104
 * INPUTS
105
 *	 $id, $notice, $category, $url, $priority, $local_only
106
 * RESULT
107
 *   Files a notice and kicks off the various alerts, smtp, telegram, pushover, system log, LED's, etc.
108
 *   If $local_only is true then the notice is not sent to external places (smtp, telegram, pushover)
109
 ******/
110
function file_notice($id, $notice, $category = "General", $url = "", $priority = 1, $local_only = false) {
111
	/*
112
	 * $category - Category that this notice should be displayed under. This can be arbitrary,
113
	 * 	       but a page must be set to receive this messages for it to be displayed.
114
	 *
115
	 * $priority - A notice's priority. Higher numbers indicate greater severity.
116
	 *	       0 = informational, 1 = warning, 2 = error, etc. This may also be arbitrary,
117
	 */
118
	global $notice_path;
119
	notices_setup();
120
	if (!$queue = get_notices()) {
121
		$queue = [];
122
	}
123
	$queuekey = time();
124
	$toqueue = array(
125
				'id'		=> htmlentities($id),
126
				'notice'	=> htmlentities($notice),
127
				'url'		=> htmlentities($url),
128
				'category'	=> htmlentities($category),
129
				'priority'	=> htmlentities($priority),
130
			);
131
	while (isset($queue[$queuekey])) {
132
		$queuekey++;
133
	}
134
	$queue[$queuekey] = $toqueue;
135
	$queueout = fopen($notice_path, "w");
136
	if (!$queueout) {
137
		log_error(sprintf(gettext("Could not open %s for writing"), $notice_path));
138
		return;
139
	}
140
	fwrite($queueout, serialize($queue));
141
	fclose($queueout);
142
	log_error(sprintf(gettext("New alert found: %s"), $notice));
143
	/* soekris */
144
	if (file_exists("/dev/led/error")) {
145
		exec("/bin/echo 1 > /dev/led/error");
146
	}
147
	/* wrap & alix */
148
	led_normalize();
149
	led_morse(1, 'sos');
150
	if (!$local_only) {
151
		notify_all_remote($notice);
152
	}
153
	return $queuekey;
154
}
155

    
156
/****f* notices/get_notices
157
 * NAME
158
 *   get_notices
159
 * INPUTS
160
 *	 $category
161
 * RESULT
162
 *   Returns a specific notices text
163
 ******/
164
function get_notices($category = "all") {
165
	global $notice_path;
166
	notices_setup();
167

    
168
	if (file_exists($notice_path)) {
169
		$queue = unserialize(file_get_contents($notice_path));
170
		if (!$queue) {
171
			return false;
172
		}
173
		if ($category != 'all') {
174
			foreach ($queue as $time => $notice) {
175
				if (strtolower($notice['category']) == strtolower($category)) {
176
					$toreturn[$time] = $notice;
177
				}
178
			}
179
			return $toreturn;
180
		} else {
181
			return $queue;
182
		}
183
	} else {
184
		return false;
185
	}
186
}
187

    
188
/****f* notices/close_notice
189
 * NAME
190
 *   close_notice
191
 * INPUTS
192
 *	 $id
193
 * RESULT
194
 *   Removes a notice from the list
195
 ******/
196
function close_notice($id) {
197
	global $notice_path;
198
	notices_setup();
199
	require_once("util.inc");
200
	/* soekris */
201
	if (file_exists("/dev/led/error")) {
202
		exec("/bin/echo 0 > /dev/led/error");
203
	}
204
	/* wrap & alix */
205
	led_normalize();
206
	$ids = [];
207
	if (!$notices = get_notices()) {
208
		return;
209
	}
210
	if ($id == "all") {
211
		@file_put_contents($notice_path, '');
212
		notices_setup();
213
		return;
214
	}
215
	foreach (array_keys($notices) as $time) {
216
		if ($id == $time) {
217
			unset($notices[$id]);
218
			break;
219
		}
220
	}
221
	foreach ($notices as $key => $notice) {
222
		$ids[$key] = $notice['id'];
223
	}
224
	foreach ($ids as $time => $tocheck) {
225
		if ($id == $tocheck) {
226
			unset($notices[$time]);
227
			break;
228
		}
229
	}
230
	if (count($notices) != 0) {
231
		$queueout = fopen($notice_path, "w");
232
		fwrite($queueout, serialize($notices));
233
		fclose($queueout);
234
	} else {
235
		@file_put_contents($notice_path, '');
236
		notices_setup();
237
	}
238

    
239
	return;
240
}
241

    
242
/****f* notices/are_notices_pending
243
 * NAME
244
 *   are_notices_pending
245
 * INPUTS
246
 *	 $category to check
247
 * RESULT
248
 *   returns true if notices are pending, false if they are not
249
 ******/
250
function are_notices_pending($category = "all") {
251
	global $notice_path;
252
	notices_setup();
253
	return file_exists($notice_path) && (filesize($notice_path) > 0);
254
}
255

    
256
function notices_sendqueue() {
257
	global $notice_queue;
258
	notices_setup();
259
	$nothing_done_count = 0;
260
	$messagequeue = [];
261

    
262
	while(true) {
263
		$nothing_done_count++;
264
		$smtpcount = 0;
265
		$messages = [];
266
		if (is_file($notice_queue) &&
267
		    is_writable($notice_queue)) {
268
			$notifyqueue_lck = lock("notifyqueue", LOCK_EX);
269
			$messages = unserialize(file_get_contents($notice_queue));
270
			if ($messages &&
271
			    is_array($messages) &&
272
			    !empty($messages)) {
273
				$messagequeue = $messages;
274
				/* Empty message queue */
275
				$messages = ['mails' => ['item' => []]];
276
				$ret = @file_put_contents($notice_queue, serialize($messages));
277
				if ($ret === false) {
278
					log_error("ERROR: Failed to write notify message queue!");
279
					return;
280
				}
281
			} else {
282
				/* No messages in queue, nothing to do */
283
				return;
284
			}
285
			unset($messages);
286
		} else {
287
			/* Queue does not exist or is not writable, so no action can be taken
288
			 * https://redmine.pfsense.org/issues/14031
289
			 */
290
			log_error("SMTP queue does not exist or is not writable.");
291
			return;
292
		}
293
		// clear lock before trying to send messages, so new one's can be added
294
		unlock($notifyqueue_lck);
295

    
296
		$smtpmessage = "";
297
		foreach(array_get_path($messagequeue, 'mails/item', []) as $mail) {
298
			if (!is_array($mail) || empty($mail)) {
299
				continue;
300
			}
301
			switch ($mail['type']) {
302
				case 'mail':
303
					$smtpcount++;
304
					$smtpmessage .= "\r\n" . date("H:i:s", $mail['time']) . " " . $mail['msg'];
305
					break;
306
				default:
307
					break;
308
			}
309
		}
310
		if (!empty($smtpmessage)) {
311
			$smtpmessageheader = sprintf(gettext("Notifications in this message: %s"), $smtpcount);
312
			$smtpmessageheader .= "\r\n" . str_repeat('=', strlen($smtpmessageheader)) . "\r\n";
313
			$nothing_done_count = 0;
314
			notify_via_smtp($smtpmessage, true, $smtpmessageheader);
315
		}
316

    
317
		/* First batch may already be sent, sleep a bit before checking
318
		 * again to send additional larger batches. */
319
		if ($nothing_done_count > 3) {
320
			break;
321
		} else {
322
			sleep(30);
323
		}
324
	}
325
}
326

    
327
function notify_via_queue_add($message, $type='mail') {
328
	global $notice_queue;
329
	notices_setup();
330
	$mail = [];
331
	$mail['time'] = time();
332
	$mail['type'] = $type;
333
	$mail['msg'] = $message;
334
	$notifyqueue_lck = lock("notifyqueue", LOCK_EX);
335
	$messages = [];
336
	if (is_file($notice_queue)) {
337
		if (!is_writable($notice_queue)) {
338
			/* Cannot write to notify queue, so exit early
339
			 * https://redmine.pfsense.org/issues/14031
340
			 */
341
			log_error("Cannot write to the notify queue.");
342
			return;
343
		}
344
		$queue = unserialize(file_get_contents($notice_queue));
345
		if ($queue) {
346
			$messages = $queue;
347
		}
348
	}
349
	if (is_array($messages)) {
350
		$msg = array_get_path($messages, 'mails/item', []);
351
		$msg[] = $mail;
352
		array_set_path($messages, 'mails/item', $msg);
353
		$ret = @file_put_contents($notice_queue, serialize($messages));
354
		if ($ret === false) {
355
			log_error("ERROR: Failed to write notify message queue!");
356
			return;
357
		}
358
	}
359
	unset($messages);
360

    
361
	mwexec_bg('/usr/local/bin/notify_monitor.php');
362
	unlock($notifyqueue_lck);
363
}
364

    
365
/****f* notices/notify_via_smtp
366
 * NAME
367
 *   notify_via_smtp
368
 * INPUTS
369
 *	 notification string to send as an email
370
 * RESULT
371
 *   returns true if message was sent
372
 ******/
373
function notify_via_smtp($message, $force = false, $header = "") {
374
	global $notice_last;
375
	if (is_platform_booting()) {
376
		return;
377
	}
378

    
379
	if ((config_path_enabled('notifications/smtp', 'disable') && !$force) ||
380
	    empty(config_get_path('notifications/smtp/ipaddress')) ||
381
	    empty(config_get_path('notifications/smtp/notifyemailaddress'))) {
382
		return;
383
	}
384

    
385
	notices_setup();
386

    
387
	/* Try not to send the same message twice, except if $force is true */
388
	$message = trim($message);
389

    
390
	$repeat = false;
391
	if (!$force &&
392
	    file_exists($notice_last) &&
393
	    (filesize($notice_last) > 0)) {
394
		$lastmsg = trim(file_get_contents($notice_last));
395

    
396
		/* Trim leading time from previous stored message */
397
		$msgmatches = [];
398
		if (preg_match('/^(([0-9]|0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]) (.*)/', $lastmsg, $msgmatches)) {
399
			$lastmsg = $msgmatches[3];
400
		}
401

    
402
		/* Trim leading time from current message (if present) */
403
		$msgmatches = [];
404
		if (preg_match('/^(([0-9]|0[0-9]|1[0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9]) (.*)/', $message, $msgmatches)) {
405
			/* Compare text without leading time, but don't change original copy */
406
			if ($lastmsg == $msgmatches[3]) {
407
				$repeat = true;
408
			}
409
		} elseif ($lastmsg == $message) {
410
			$repeat = true;
411
		}
412
	}
413

    
414
	/* Log that we have suppressed a repeat message
415
	 * TODO: Maybe track repeats and send every X times.
416
	 */
417
	if ($repeat) {
418
		log_error(gettext("Suppressing repeat e-mail notification message."));
419
		return;
420
	}
421

    
422
	/* Store last message sent to avoid spamming */
423
	@file_put_contents($notice_last, $message);
424
	if (!$force) {
425
		notify_via_queue_add($header . $message, 'mail');
426
		$ret = true;
427
	} else {
428
		$ret = send_smtp_message($header . "\r\n" . $message . "\r\n",
429
					config_get_path('system/hostname') . '.' .
430
					config_get_path('system/domain') .
431
					" - Notification",
432
					$force);
433
	}
434

    
435
	return $ret;
436
}
437

    
438
function send_smtp_message($message, $subject = "(no subject)", $force = false) {
439
	require_once("Mail.php");
440

    
441
	/* Bail if disabled (and not forced) or if config is incomplete */
442
	if ((config_path_enabled('notifications/smtp', 'disable') && !$force) ||
443
	    empty(config_get_path('notifications/smtp/ipaddress')) ||
444
	    empty(config_get_path('notifications/smtp/notifyemailaddress'))) {
445
		return;
446
	}
447

    
448
	if (empty(config_get_path('notifications/smtp/username')) ||
449
	    empty(config_get_path('notifications/smtp/password'))) {
450
		$auth = false;
451
		$username = '';
452
		$password = '';
453
	} else {
454
		$auth = config_get_path('notifications/smtp/authentication_mechanism', 'PLAIN');
455
		$username = config_get_path('notifications/smtp/username');
456
		$password = config_get_path('notifications/smtp/password');
457
	}
458

    
459
	$params = array(
460
		'host' => (config_path_enabled('notifications/smtp', 'ssl')
461
		    ? 'ssl://'
462
		    : '')
463
		    . config_get_path('notifications/smtp/ipaddress'),
464
		'port' => config_get_path('notifications/smtp/port', 25),
465
		'auth' => $auth,
466
		'username' => $username,
467
		'password' => $password,
468
		'localhost' => config_get_path('system/hostname') . '.' . config_get_path('system/domain'),
469
		'timeout' => config_get_path('notifications/smtp/timeout', 20),
470
		'debug' => false,
471
		'persist' => false
472
	);
473

    
474
	if (config_get_path('notifications/smtp/sslvalidate') == "disabled") {
475
		$params['socket_options'] = array(
476
			'ssl' => array(
477
				'verify_peer_name' => false,
478
				'verify_peer' => false
479
		));
480
	}
481

    
482
	if (!empty(config_get_path('notifications/smtp/fromaddress'))) {
483
		$from = config_get_path('notifications/smtp/fromaddress');
484
	} else {
485
		$from = "pfsense@" . config_get_path('system/hostname') . '.' . config_get_path('system/domain');
486
	}
487

    
488
	$to = config_get_path('notifications/smtp/notifyemailaddress');
489

    
490
	$headers = array(
491
		"From"    => $from,
492
		"To"      => $to,
493
		"Subject" => $subject,
494
		"Date"    => date("r")
495
	);
496

    
497
	$error_text = 'Could not send the message to %1$s -- Error: %2$s';
498
	try {
499
		$smtp =& Mail::factory('smtp', $params);
500
		$mail = @$smtp->send($to, $headers, $message);
501

    
502
		if (PEAR::isError($mail)) {
503
			$err_msg = sprintf(gettext($error_text),
504
			    $to, $mail->getMessage());
505
		}
506
	} catch (Exception $e) {
507
		$err_msg = sprintf(gettext($error_text), $to, $e->getMessage());
508
	}
509

    
510
	if (!empty($err_msg)) {
511
		log_error($err_msg);
512
		return($err_msg);
513
	}
514

    
515
	log_error(sprintf(gettext("Message sent to %s OK"), $to));
516
	return;
517
}
518
/****f* notices/notify_via_telegram
519
 * NAME
520
 *   notify_via_telegram
521
 * INPUTS
522
 *	 notification string to send to Telegram via API
523
 * RESULT
524
 *   returns NULL if message was sent
525
 ******/
526

    
527
function notify_via_telegram($message, $force = false) {
528
	if ((!config_path_enabled('notifications/telegram', 'enabled') && (!$force)) ||
529
	    empty(config_get_path('notifications/telegram/api')) ||
530
	    empty(config_get_path('notifications/telegram/chatid'))) {
531
		if ($force) {
532
			return gettext("Unable to test Telegram notification without both API Key & Chat ID set");
533
		}
534
		return;
535
	}
536

    
537
	$url = "https://api.telegram.org/bot" . config_get_path('notifications/telegram/api') . "/sendMessage?";
538
	$data = array(
539
		"chat_id" => config_get_path('notifications/telegram/chatid'),
540
		"text" => config_get_path('system/hostname') . '.' . config_get_path('system/domain') . "\n{$message}"
541
	);
542
	$result = json_decode(curl_post_notification($url . http_build_query($data)), true);
543
	if (is_array($result)) {
544
		if ($result['ok']) {
545
			unset($err_msg);
546
		} else {
547
			$err_msg = sprintf(gettext("Failed to send Telegram notification. Error received was :{$result['error_code']}: {$result['description']}"));
548
			log_error($err_msg);
549
		}
550
	} else {
551
		$err_msg = gettext("API to Telegram did not return data in expected format!");
552
		log_error($err_msg);
553
	}
554
	return $err_msg;
555
}
556

    
557
/****f* notices/notify_via_pushover
558
 * NAME
559
 *   notify_via_pushover
560
 * INPUTS
561
 *	 notification string to send to Pushover via API
562
 * RESULT
563
 *   returns NULL if message was sent
564
 ******/
565

    
566
function notify_via_pushover($message, $force = false) {
567
	if ((!config_path_enabled('notifications/pushover', 'enabled') && (!$force)) ||
568
	    empty(config_get_path('notifications/pushover/apikey')) ||
569
	    empty(config_get_path('notifications/pushover/userkey'))) {
570
		if ($force) {
571
			return gettext("Unable to test Pushover notification without both API Key & User Key set");
572
		}
573
		return;
574
	}
575

    
576
	if (strcasecmp(config_get_path('notifications/pushover/sound'), 'devicedefault') == 0) {
577
		config_del_path('notifications/pushover/sound');
578
	}
579

    
580
	$url = "https://api.pushover.net/1/messages.json";
581
	$data = array(
582
		"token"    => config_get_path('notifications/pushover/apikey'),
583
		"user"     => config_get_path('notifications/pushover/userkey'),
584
		"sound"    => config_get_path('notifications/pushover/sound'),
585
		"priority" => config_get_path('notifications/pushover/priority'),
586
		"retry"    => config_get_path('notifications/pushover/retry'),
587
		"expire"   => config_get_path('notifications/pushover/expire'),
588
		"message"  => config_get_path('system/hostname') . '.' . config_get_path('system/domain') . "\n{$message}"
589
	);
590
	$result = json_decode(curl_post_notification($url, $data), true);
591
	if (is_array($result)) {
592
		if ($result['status']) {
593
			unset($err_msg);
594
		} else {
595
			$err_msg = sprintf(gettext("Failed to send Pushover notification. Error received was: %s"), $result['errors']['0']);
596
			log_error($err_msg);
597
		}
598
	} else {
599
		$err_msg = gettext("Pushover API server did not return data in expected format!");
600
		log_error($err_msg);
601
	}
602
	return $err_msg;
603
}
604

    
605
/****f* notices/notify_via_slack
606
 * NAME
607
 *   notify_via_slack
608
 * INPUTS
609
 *	 notification string to send to Slack via API
610
 * RESULT
611
 *   returns NULL if message was sent
612
 ******/
613

    
614
function notify_via_slack($message, $force = false) {
615
	if ((!config_path_enabled('notifications/slack', 'enabled') && (!$force)) ||
616
	    empty(config_get_path('notifications/slack/api')) ||
617
	    empty(config_get_path('notifications/slack/channel'))) {
618
		if ($force) {
619
			return gettext("Unable to test Slack notification without both API Key & Channel set");
620
		}
621
		return;
622
	}
623

    
624
	$url = "https://slack.com/api/chat.postMessage";
625
	$data = array(
626
		"token"    => config_get_path('notifications/slack/api'),
627
		"channel"  => "#" . config_get_path('notifications/slack/channel'),
628
		"text"     => $message,
629
		"username" => config_get_path('system/hostname') . '.' . config_get_path('system/domain')
630
	);
631
	$result = json_decode(curl_post_notification($url, $data), true);
632
	if (is_array($result)) {
633
		if ($result['ok']) {
634
			unset($err_msg);
635
		} else {
636
			$err_msg = sprintf(gettext("Failed to send Slack notification. Error received was: %s"), $result['error']);
637
			log_error($err_msg);
638
		}
639
	} else {
640
		$err_msg = gettext("Slack API server did not return data in expected format!");
641
		log_error($err_msg);
642
	}
643
	return $err_msg;
644
}
645

    
646
function curl_post_notification($url, $data = []) {
647
	$conn = curl_init($url);
648
	if (!empty($data)) {
649
		curl_setopt($conn, CURLOPT_POSTFIELDS, $data);
650
	}
651
	curl_setopt($conn, CURLOPT_SSL_VERIFYPEER, true);
652
	curl_setopt($conn, CURLOPT_FRESH_CONNECT,  true);
653
	curl_setopt($conn, CURLOPT_RETURNTRANSFER, true);
654
	set_curlproxy($conn);
655
	$curl_post_result = curl_exec($conn);
656
	curl_close($conn);
657
	return $curl_post_result; //json encoded
658
}
659

    
660
/* Notify via remote methods only - not via GUI. */
661
function notify_all_remote($msg) {
662
	notify_via_smtp($msg);
663
	notify_via_telegram($msg);
664
	notify_via_pushover($msg);
665
	notify_via_slack($msg);
666
}
(31-31/61)