Project

General

Profile

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

    
29
/* include all configuration functions */
30
if (!function_exists('captiveportal_syslog')) {
31
	require_once("captiveportal.inc");
32
}
33

    
34
function captiveportal_xmlrpc_sync_get_details(&$syncip, &$port, &$username, &$password, $carp_loop = false) {
35
	global $cpzone;
36

    
37
	if (config_path_enabled("captiveportal/{$cpzone}", "enablebackwardsync") && !$carp_loop
38
	    && captiveportal_ha_is_node_in_backup_mode($cpzone)) {
39
		$syncip = config_get_path("captiveportal/{$cpzone}/backwardsyncip");
40
		$username = config_get_path("captiveportal/{$cpzone}/backwardsyncuser");
41
		$password = config_get_path("captiveportal/{$cpzone}/backwardsyncpassword");
42
		$port = config_get_path('system/webgui/port');
43
		if (empty($port)) { // if port is empty lets rely on the protocol selection
44
			if (config_get_path('system/webgui/protocol') == "http") {
45
				$port = "80";
46
			} else {
47
				$port = "443";
48
			}
49
		}
50
		return true;
51
	}
52

    
53
	if (empty(config_get_path('hasync/synchronizetoip')) ||
54
	    config_get_path('hasync/synchronizecaptiveportal') == "" ||
55
	    $carp_loop == true) {
56
		return false;
57
	}
58

    
59
	$syncip = config_get_path('hasync/synchronizetoip');
60
	$password = config_get_path('hasync/password');
61
	if (empty(config_get_path('hasync/username'))) {
62
		$username = "admin";
63
	} else {
64
		$username = config_get_path('hasync/username');
65
	}
66

    
67
	/* if port is empty lets rely on the protocol selection */
68
	$port = config_get_path('system/webgui/port');
69
	if (empty($port)) {
70
		if (config_get_path('system/webgui/protocol') == "http") {
71
			$port = "80";
72
		} else {
73
			$port = "443";
74
		}
75
	}
76

    
77
	return true;
78
}
79

    
80
function voucher_expire($voucher_received, $carp_loop = false) {
81
	global $g, $cpzone, $cpzoneid;
82

    
83
	$voucherlck = lock("voucher{$cpzone}", LOCK_EX);
84

    
85
	// read rolls into assoc array with rollid as key and minutes as value
86
	$tickets_per_roll = array();
87
	$minutes_per_roll = array();
88
	foreach (config_get_path("voucher/{$cpzone}/roll", []) as $rollent) {
89
		$tickets_per_roll[$rollent['number']] = $rollent['count'];
90
		$minutes_per_roll[$rollent['number']] = $rollent['minutes'];
91
	}
92

    
93
	// split into an array. Useful for multiple vouchers given
94
	$a_vouchers_received = preg_split("/[\t\n\r ]+/s", $voucher_received);
95
	$active_dirty = false;
96
	$unsetindexes = array();
97

    
98
	// go through all received vouchers, check their valid and extract
99
	// Roll# and Ticket# using the external readvoucher binary
100
	foreach ($a_vouchers_received as $voucher) {
101
		$v = escapeshellarg($voucher);
102
		if (strlen($voucher) < 5) {
103
			captiveportal_syslog("{$voucher} invalid: Too short!");
104
			continue;   // seems too short to be a voucher!
105
		}
106

    
107
		unset($output);
108
		$_gb = exec("/usr/local/bin/voucher -c {$g['varetc_path']}/voucher_{$cpzone}.cfg -k {$g['varetc_path']}/voucher_{$cpzone}.public -- $v", $output);
109
		list($status, $roll, $nr) = explode(" ", $output[0]);
110
		if ($status == "OK") {
111
			// check if we have this ticket on a registered roll for this ticket
112
			if ($tickets_per_roll[$roll] && ($nr <= $tickets_per_roll[$roll])) {
113
				// voucher is from a registered roll.
114
				if (!isset($active_vouchers[$roll])) {
115
					$active_vouchers[$roll] = voucher_read_active_db($roll);
116
				}
117
				// valid voucher. Store roll# and ticket#
118
				if (!empty($active_vouchers[$roll][$voucher])) {
119
					$active_dirty = true;
120
					unset($active_vouchers[$roll][$voucher]);
121
				}
122
				// check if voucher already marked as used
123
				if (!isset($bitstring[$roll])) {
124
					$bitstring[$roll] = voucher_read_used_db($roll);
125
				}
126
				$pos = $nr >> 3; // divide by 8 -> octet
127
				$mask = 1 << ($nr % 8);
128
				// mark bit for this voucher as used
129
				if (!(ord($bitstring[$roll][$pos]) & $mask)) {
130
					$bitstring[$roll][$pos] = chr(ord($bitstring[$roll][$pos]) | $mask);
131
				}
132
				captiveportal_syslog("{$voucher} ({$roll}/{$nr}) forced to expire");
133

    
134
				/* Check if this voucher has any active sessions */
135
				$cpentry = captiveportal_read_db("WHERE username = '{$voucher}'");
136
				if (!empty($cpentry) && !empty($cpentry[0])) {
137
					if (empty($cpzoneid) && !empty(config_get_path("captiveportal/{$cpzone}"))) {
138
						$cpzoneid = config_get_path("captiveportal/{$cpzone}/zoneid");
139
					}
140
					$cpentry = $cpentry[0];
141
					captiveportal_disconnect($cpentry, 13);
142
					captiveportal_logportalauth($cpentry[4], $cpentry[3], $cpentry[2], "FORCEFULLY TERMINATING VOUCHER {$voucher} SESSION");
143
					$unsetindexes[] = $cpentry[5];
144
				}
145
			} else {
146
				captiveportal_syslog(sprintf(gettext('%1$s (%2$s/%3$s): not found on any registered Roll'), $voucher, $roll, $nr));
147
			}
148
		} else {
149
			// hmm, thats weird ... not what I expected
150
			captiveportal_syslog(sprintf(gettext('%1$s invalid: %2$s!!'), $voucher, $output[0]));
151
		}
152
	}
153

    
154
	// Refresh active DBs
155
	if ($active_dirty == true) {
156
		foreach ($active_vouchers as $roll => $active) {
157
			voucher_write_active_db($roll, $active);
158
		}
159
		/* perform in-use vouchers expiration using check_reload_status */
160
		send_event("service sync vouchers");
161
	} else {
162
		$active_vouchers = array();
163
	}
164

    
165
	// Write back the used DB's
166
	if (is_array($bitstring)) {
167
		foreach ($bitstring as $roll => $used) {
168
			if (is_array($used)) {
169
				foreach ($used as $u) {
170
					voucher_write_used_db($roll, base64_encode($u));
171
				}
172
			} else {
173
				voucher_write_used_db($roll, base64_encode($used));
174
			}
175
		}
176
	}
177

    
178
	unlock($voucherlck);
179

    
180
	/* Write database */
181
	if (!empty($unsetindexes)) {
182
		captiveportal_remove_entries($unsetindexes);
183
	}
184

    
185
	// XMLRPC Call over to the other node
186
	if (captiveportal_xmlrpc_sync_get_details($syncip, $syncport,
187
	    $syncuser, $syncpass, $carp_loop)) {
188
		$rpc_client = new pfsense_xmlrpc_client();
189
		$rpc_client->setConnectionData($syncip, $syncport, $syncuser, $syncpass);
190
		$rpc_client->set_noticefile("CaptivePortalVouchersSync");
191
		$arguments = array(
192
			'active_and_used_vouchers_bitmasks' => $bitstring,
193
			'active_vouchers' => $active_vouchers
194
		);
195

    
196
		$rpc_client->xmlrpc_method('captive_portal_sync',
197
			array(
198
				'op' => 'write_vouchers',
199
				'zone' => $cpzone,
200
				'arguments' => base64_encode(serialize($arguments))
201
			)
202
		);
203
	}
204
	return true;
205
}
206

    
207
/*
208
 * Authenticate a voucher and return the remaining time credit in minutes
209
 * if $test is set, don't mark the voucher as used nor add it to the list
210
 * of active vouchers
211
 * If $test is set, simply test the voucher. Don't change anything
212
 * but return a more verbose error and result message back
213
 */
214
function voucher_auth($voucher_received, $test = 0, $carp_loop = false) {
215
	global $g, $cpzone, $dbc;
216

    
217
	if (!config_path_enabled("voucher/{$cpzone}")) {
218
		return 0;
219
	}
220

    
221
	$voucherlck = lock("voucher{$cpzone}", LOCK_EX);
222

    
223
	// read rolls into assoc array with rollid as key and minutes as value
224
	$tickets_per_roll = array();
225
	$minutes_per_roll = array();
226
	foreach (config_get_path("voucher/{$cpzone}/roll", []) as $rollent) {
227
		$tickets_per_roll[$rollent['number']] = $rollent['count'];
228
		$minutes_per_roll[$rollent['number']] = $rollent['minutes'];
229
	}
230

    
231
	// split into an array. Useful for multiple vouchers given
232
	$a_vouchers_received = preg_split("/[\t\n\r ]+/s", $voucher_received);
233
	$error = 0;
234
	$test_result = array();     // used to display for voucher test option in GUI
235
	$total_minutes = 0;
236
	$first_voucher = "";
237
	$first_voucher_roll = 0;
238

    
239
	// go through all received vouchers, check their valid and extract
240
	// Roll# and Ticket# using the external readvoucher binary
241
	foreach ($a_vouchers_received as $voucher) {
242
		$v = escapeshellarg($voucher);
243
		if (strlen($voucher) < 5) {
244
			$voucher_err_text = sprintf(gettext("%s invalid: Too short!"), $voucher);
245
			$test_result[] = $voucher_err_text;
246
			captiveportal_syslog($voucher_err_text);
247
			$error++;
248
			continue;   // seems too short to be a voucher!
249
		}
250

    
251
		$result = exec("/usr/local/bin/voucher -c {$g['varetc_path']}/voucher_{$cpzone}.cfg -k {$g['varetc_path']}/voucher_{$cpzone}.public -- $v");
252
		list($status, $roll, $nr) = explode(" ", $result);
253
		if ($status == "OK") {
254
			if (!$first_voucher) {
255
				// store first voucher. Thats the one we give the timecredit
256
				$first_voucher = $voucher;
257
				$first_voucher_roll = $roll;
258
			}
259
			// check if we have this ticket on a registered roll for this ticket
260
			if ($tickets_per_roll[$roll] && ($nr <= $tickets_per_roll[$roll])) {
261
				// voucher is from a registered roll.
262
				if (!isset($active_vouchers[$roll])) {
263
					$active_vouchers[$roll] = voucher_read_active_db($roll);
264
				}
265
				// valid voucher. Store roll# and ticket#
266
				if (!empty($active_vouchers[$roll][$voucher])) {
267
					list($timestamp, $minutes) = explode(",", $active_vouchers[$roll][$voucher]);
268
					// we have an already active voucher here.
269
					$remaining = intval((($timestamp + (60*$minutes)) - time())/60);
270
					$test_result[] = sprintf(gettext('%1$s (%2$s/%3$s) active and good for %4$d Minutes'), $voucher, $roll, $nr, $remaining);
271
					$total_minutes += $remaining;
272
				} else {
273
					// voucher not used. Check if ticket Id is on the roll (not too high)
274
					// and if the ticket is marked used.
275
					// check if voucher already marked as used
276
					if (!isset($bitstring[$roll])) {
277
						$bitstring[$roll] = voucher_read_used_db($roll);
278
					}
279
					$pos = $nr >> 3; // divide by 8 -> octet
280
					$mask = 1 << ($nr % 8);
281
					if (ord($bitstring[$roll][$pos]) & $mask) {
282
						$voucher_err_text = sprintf(gettext('%1$s (%2$s/%3$s) already used and expired'), $voucher, $roll, $nr);
283
						$test_result[] = $voucher_err_text;
284
						captiveportal_syslog($voucher_err_text);
285
						$total_minutes = -1;    // voucher expired
286
						$error++;
287
					} else {
288
						// mark bit for this voucher as used
289
						$bitstring[$roll][$pos] = chr(ord($bitstring[$roll][$pos]) | $mask);
290
						$test_result[] = sprintf(gettext('%1$s (%2$s/%3$s) good for %4$s Minutes'), $voucher, $roll, $nr, $minutes_per_roll[$roll]);
291
						$total_minutes += $minutes_per_roll[$roll];
292
					}
293
				}
294
			} else {
295
				$voucher_err_text = sprintf(gettext('%1$s (%2$s/%3$s): not found on any registered Roll'), $voucher, $roll, $nr);
296
				$test_result[] = $voucher_err_text;
297
				captiveportal_syslog($voucher_err_text);
298
			}
299
		} else {
300
			// hmm, thats weird ... not what I expected
301
			$voucher_err_text = sprintf(gettext('%1$s invalid: %2$s !!'), $voucher, $result);
302
			$test_result[] = $voucher_err_text;
303
			captiveportal_syslog($voucher_err_text);
304
			$error++;
305
		}
306
	}
307

    
308
	// if this was a test call, we're done. Return the result.
309
	if ($test) {
310
		if ($error) {
311
			$test_result[] = gettext("Access denied!");
312
		} else {
313
			$test_result[] = sprintf(gettext("Access granted for %d Minutes in total."), $total_minutes);
314
		}
315
		unlock($voucherlck);
316

    
317
		return $test_result;
318
	}
319

    
320
	// if we had an error (one of the vouchers is invalid), return 0.
321
	// Discussion: we could return the time remaining for good vouchers, but then
322
	// the user wouldn't know that he used at least one invalid voucher.
323
	if ($error) {
324
		unlock($voucherlck);
325
		if ($total_minutes > 0) {   // probably not needed, but want to make sure
326
			$total_minutes = 0;     // we only report -1 (expired) or 0 (no access)
327
		}
328
		return $total_minutes;       // well, at least one voucher had errors. Say NO ACCESS
329
	}
330

    
331
	// All given vouchers were valid and this isn't simply a test.
332
	// Write back the used DB's
333
	if (is_array($bitstring)) {
334
		foreach ($bitstring as $roll => $used) {
335
			if (is_array($used)) {
336
				foreach ($used as $u) {
337
					voucher_write_used_db($roll, base64_encode($u));
338
				}
339
			} else {
340
				voucher_write_used_db($roll, base64_encode($used));
341
			}
342
		}
343
	}
344

    
345
	// Active DB: we only add the first voucher if multiple given
346
	// and give that one all the time credit. This allows the user to logout and
347
	// log in later using just the first voucher. It also keeps username limited
348
	// to one voucher and that voucher shows the correct time credit in 'active vouchers'
349
	if (!empty($active_vouchers[$first_voucher_roll][$first_voucher])) {
350
		list($timestamp, $minutes) = explode(",", $active_vouchers[$first_voucher_roll][$first_voucher]);
351
	} else {
352
		$timestamp = time();    // new voucher
353
		$minutes = $total_minutes;
354
	}
355

    
356
	$active_vouchers[$first_voucher_roll][$first_voucher] = "$timestamp,$minutes";
357
	voucher_write_active_db($first_voucher_roll, $active_vouchers[$first_voucher_roll]);
358

    
359
	// XMLRPC Call over to the other node
360
	if (captiveportal_xmlrpc_sync_get_details($syncip, $syncport,
361
	    $syncuser, $syncpass, $carp_loop)) {
362
		$rpc_client = new pfsense_xmlrpc_client();
363
		$rpc_client->setConnectionData($syncip, $syncport, $syncuser, $syncpass);
364
		$rpc_client->set_noticefile("CaptivePortalVouchersSync");
365
		$arguments = array(
366
			'active_and_used_vouchers_bitmasks' => $bitstring,
367
			'active_vouchers' => array(
368
				$first_voucher_roll => $active_vouchers[$first_voucher_roll]
369
			)
370
		);
371

    
372
		$rpc_client->xmlrpc_method('captive_portal_sync',
373
			array(
374
				'op' => 'write_vouchers',
375
				'zone' => $cpzone,
376
				'arguments' => base64_encode(serialize($arguments))
377
			)
378
		);
379
	}
380

    
381
	/* perform in-use vouchers expiration using check_reload_status */
382
	send_event("service sync vouchers");
383

    
384
	unlock($voucherlck);
385

    
386
	return $total_minutes;
387
}
388

    
389
function voucher_configure() {
390
	global $g, $cpzone;
391

    
392
	foreach (config_get_path('voucher', []) as $voucherzone => $vcfg) {
393
		if (is_platform_booting()) {
394
			echo gettext("Enabling voucher support... ");
395
		}
396
		$cpzone = $voucherzone;
397
		$error = voucher_configure_zone();
398
		if (is_platform_booting()) {
399
			if ($error) {
400
				echo "error\n";
401
			} else {
402
				echo "done\n";
403
			}
404
		}
405
	}
406
}
407

    
408
function voucher_configure_zone() {
409
	global $g, $cpzone;
410

    
411
	if (!config_path_enabled("voucher/{$cpzone}")) {
412
		return 0;
413
	}
414

    
415
	$voucherlck = lock("voucher{$cpzone}", LOCK_EX);
416

    
417
	/* write public key used to verify vouchers */
418
	$pubkey = base64_decode(config_get_path("voucher/{$cpzone}/publickey"));
419
	$fd = fopen("{$g['varetc_path']}/voucher_{$cpzone}.public", "w");
420
	if (!$fd) {
421
		captiveportal_syslog("Voucher error: cannot write voucher.public");
422
		unlock($voucherlck);
423
		return 1;
424
	}
425
	fwrite($fd, $pubkey);
426
	fclose($fd);
427
	@chmod("{$g['varetc_path']}/voucher_{$cpzone}.public", 0600);
428

    
429
	/* write config file used by voucher binary to decode vouchers */
430
	$fd = fopen("{$g['varetc_path']}/voucher_{$cpzone}.cfg", "w");
431
	if (!$fd) {
432
		printf(gettext("Error: cannot write voucher.cfg") . "\n");
433
		unlock($voucherlck);
434
		return 1;
435
	}
436
	$voucher_config = config_get_path("voucher/{$cpzone}");
437
	fwrite($fd, "{$voucher_config['rollbits']},{$voucher_config['ticketbits']},{$voucher_config['checksumbits']},{$voucher_config['magic']},{$voucher_config['charset']}\n");
438
	fclose($fd);
439
	@chmod("{$g['varetc_path']}/voucher_{$cpzone}.cfg", 0600);
440
	unlock($voucherlck);
441

    
442
	return 0;
443
}
444

    
445
/* write bitstring of used vouchers to ramdisk.
446
 * Bitstring must already be base64_encoded!
447
 */
448
function voucher_write_used_db($roll, $vdb) {
449
	global $g, $cpzone;
450

    
451
	$fd = fopen("{$g['vardb_path']}/voucher_{$cpzone}_used_$roll.db", "w");
452
	if ($fd) {
453
		fwrite($fd, $vdb . "\n");
454
		fclose($fd);
455
	} else {
456
		voucher_log(LOG_ERR, sprintf(gettext('cant write %1$s/voucher_%2$s_used_%3$s.db'), g_get('vardb_path'), $cpzone, $roll));
457
	}
458
}
459

    
460
/* return assoc array of active vouchers with activation timestamp
461
 * voucher is index.
462
 */
463
function voucher_read_active_db($roll) {
464
	global $g, $cpzone;
465

    
466
	$active = array();
467
	$dirty = 0;
468
	$file = "{$g['vardb_path']}/voucher_{$cpzone}_active_$roll.db";
469
	if (file_exists($file)) {
470
		$fd = fopen($file, "r");
471
		if ($fd) {
472
			while (!feof($fd)) {
473
				$line = trim(fgets($fd));
474
				if ($line) {
475
					list($voucher, $timestamp, $minutes) = explode(",", $line); // voucher,timestamp
476
					if ((($timestamp + (60*$minutes)) - time()) > 0) {
477
						$active[$voucher] = "$timestamp,$minutes";
478
					} else {
479
						$dirty=1;
480
					}
481
				}
482
			}
483
			fclose($fd);
484
			if ($dirty) { // if we found expired entries, lets remove them
485
				voucher_write_active_db($roll, $active);
486
			}
487
		}
488
	}
489
	return $active;
490
}
491

    
492
/* store array of active vouchers back to DB */
493
function voucher_write_active_db($roll, $active) {
494
	global $g, $cpzone;
495

    
496
	if (!is_array($active)) {
497
		return;
498
	}
499
	$fd = fopen("{$g['vardb_path']}/voucher_{$cpzone}_active_$roll.db", "w");
500
	if ($fd) {
501
		foreach ($active as $voucher => $value) {
502
			fwrite($fd, "$voucher,$value\n");
503
		}
504
		fclose($fd);
505
	}
506
}
507

    
508
/* return how many vouchers are marked used on a roll */
509
function voucher_used_count($roll) {
510
	global $g, $cpzone;
511

    
512
	$bitstring = voucher_read_used_db($roll);
513
	$max = strlen($bitstring) * 8;
514
	$used = 0;
515
	for ($i = 1; $i <= $max; $i++) {
516
		// check if ticket already used or not.
517
		$pos = $i >> 3;            // divide by 8 -> octet
518
		$mask = 1 << ($i % 8);  // mask to test bit in octet
519
		if (ord($bitstring[$pos]) & $mask) {
520
			$used++;
521
		}
522
	}
523
	unset($bitstring);
524

    
525
	return $used;
526
}
527

    
528
function voucher_read_used_db($roll) {
529
	global $g, $cpzone;
530

    
531
	$vdb = "";
532
	$file = "{$g['vardb_path']}/voucher_{$cpzone}_used_$roll.db";
533
	if (file_exists($file)) {
534
		$fd = fopen($file, "r");
535
		if ($fd) {
536
			$vdb = trim(fgets($fd));
537
			fclose($fd);
538
		} else {
539
			voucher_log(LOG_ERR, sprintf(gettext('cant read %1$s/voucher_%2$s_used_%3$s.db'), g_get('vardb_path'), $cpzone, $roll));
540
		}
541
	}
542
	return base64_decode($vdb);
543
}
544

    
545
function voucher_unlink_db($roll) {
546
	global $g, $cpzone;
547
	@unlink("{$g['vardb_path']}/voucher_{$cpzone}_used_$roll.db");
548
	@unlink("{$g['vardb_path']}/voucher_{$cpzone}_active_$roll.db");
549
}
550

    
551
/* we share the log with captiveportal for now */
552
function voucher_log($priority, $message) {
553

    
554
	$message = trim($message);
555
	openlog("logportalauth", LOG_PID, LOG_LOCAL4);
556
	syslog($priority, sprintf(gettext("Voucher: %s"), $message));
557
	closelog();
558
}
559

    
560
/* Perform natural expiration of vouchers
561
 * Called during reboot -> system_reboot_cleanup() and every active voucher change
562
 */
563
function voucher_save_db_to_config() {
564
	global $g;
565

    
566
	foreach (config_get_path('voucher', []) as $zone => $vcfg) {
567
		if (config_path_enabled("voucher/{$zone}")) {
568
			foreach (config_get_path("voucher/{$zone}/roll", []) as $key => $rollent) {
569
				// read_active_db will remove expired vouchers that are still active
570
				voucher_read_active_db($rollent['number']);
571
			}
572
		}
573
	}
574
}
575

    
576
?>
(55-55/61)