Project

General

Profile

Download (18.7 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-2020 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 $config, $cpzone;
36

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

    
53
	if (!is_array($config['hasync']) ||
54
	    empty($config['hasync']['synchronizetoip']) ||
55
	    $config['hasync']['synchronizecaptiveportal'] == "" ||
56
	    $carp_loop == true) {
57
		return false;
58
	}
59

    
60
	$syncip = $config['hasync']['synchronizetoip'];
61
	$password = $config['hasync']['password'];
62
	if (empty($config['hasync']['username'])) {
63
		$username = "admin";
64
	} else {
65
		$username = $config['hasync']['username'];
66
	}
67

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

    
78
	return true;
79
}
80

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

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

    
86
	// read rolls into assoc array with rollid as key and minutes as value
87
	$tickets_per_roll = array();
88
	$minutes_per_roll = array();
89
	if (is_array($config['voucher'][$cpzone]['roll'])) {
90
		foreach ($config['voucher'][$cpzone]['roll'] as $rollent) {
91
			$tickets_per_roll[$rollent['number']] = $rollent['count'];
92
			$minutes_per_roll[$rollent['number']] = $rollent['minutes'];
93
		}
94
	}
95

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

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

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

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

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

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

    
181
	unlock($voucherlck);
182

    
183
	/* Write database */
184
	if (!empty($unsetindexes)) {
185
		captiveportal_remove_entries($unsetindexes);
186
	}
187

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

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

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

    
220
	if (!isset($config['voucher'][$cpzone]['enable'])) {
221
		return 0;
222
	}
223

    
224
	$voucherlck = lock("voucher{$cpzone}", LOCK_EX);
225

    
226
	// read rolls into assoc array with rollid as key and minutes as value
227
	$tickets_per_roll = array();
228
	$minutes_per_roll = array();
229
	if (is_array($config['voucher'][$cpzone]['roll'])) {
230
		foreach ($config['voucher'][$cpzone]['roll'] as $rollent) {
231
			$tickets_per_roll[$rollent['number']] = $rollent['count'];
232
			$minutes_per_roll[$rollent['number']] = $rollent['minutes'];
233
		}
234
	}
235

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

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

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

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

    
322
		return $test_result;
323
	}
324

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

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

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

    
361
	$active_vouchers[$first_voucher_roll][$first_voucher] = "$timestamp,$minutes";
362
	voucher_write_active_db($first_voucher_roll, $active_vouchers[$first_voucher_roll]);
363

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

    
377
		$rpc_client->xmlrpc_method('captive_portal_sync',
378
			array(
379
				'op' => 'write_vouchers',
380
				'zone' => $cpzone,
381
				'arguments' => base64_encode(serialize($arguments))
382
			)
383
		);
384
	}
385

    
386
	/* perform in-use vouchers expiration using check_reload_status */
387
	send_event("service sync vouchers");
388

    
389
	unlock($voucherlck);
390

    
391
	return $total_minutes;
392
}
393

    
394
function voucher_configure() {
395
	global $config, $g, $cpzone;
396

    
397
	if (is_array($config['voucher'])) {
398
		foreach ($config['voucher'] as $voucherzone => $vcfg) {
399
			if (platform_booting()) {
400
				echo gettext("Enabling voucher support... ");
401
			}
402
			$cpzone = $voucherzone;
403
			$error = voucher_configure_zone();
404
			if (platform_booting()) {
405
				if ($error) {
406
					echo "error\n";
407
				} else {
408
					echo "done\n";
409
				}
410
			}
411
		}
412
	}
413
}
414

    
415
function voucher_configure_zone() {
416
	global $config, $g, $cpzone;
417

    
418
	if (!isset($config['voucher'][$cpzone]['enable'])) {
419
		return 0;
420
	}
421

    
422
	$voucherlck = lock("voucher{$cpzone}", LOCK_EX);
423

    
424
	/* write public key used to verify vouchers */
425
	$pubkey = base64_decode($config['voucher'][$cpzone]['publickey']);
426
	$fd = fopen("{$g['varetc_path']}/voucher_{$cpzone}.public", "w");
427
	if (!$fd) {
428
		captiveportal_syslog("Voucher error: cannot write voucher.public");
429
		unlock($voucherlck);
430
		return 1;
431
	}
432
	fwrite($fd, $pubkey);
433
	fclose($fd);
434
	@chmod("{$g['varetc_path']}/voucher_{$cpzone}.public", 0600);
435

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

    
448
	return 0;
449
}
450

    
451
/* write bitstring of used vouchers to ramdisk.
452
 * Bitstring must already be base64_encoded!
453
 */
454
function voucher_write_used_db($roll, $vdb) {
455
	global $g, $cpzone;
456

    
457
	$fd = fopen("{$g['vardb_path']}/voucher_{$cpzone}_used_$roll.db", "w");
458
	if ($fd) {
459
		fwrite($fd, $vdb . "\n");
460
		fclose($fd);
461
	} else {
462
		voucher_log(LOG_ERR, sprintf(gettext('cant write %1$s/voucher_%2$s_used_%3$s.db'), $g['vardb_path'], $cpzone, $roll));
463
	}
464
}
465

    
466
/* return assoc array of active vouchers with activation timestamp
467
 * voucher is index.
468
 */
469
function voucher_read_active_db($roll) {
470
	global $g, $cpzone;
471

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

    
498
/* store array of active vouchers back to DB */
499
function voucher_write_active_db($roll, $active) {
500
	global $g, $cpzone;
501

    
502
	if (!is_array($active)) {
503
		return;
504
	}
505
	$fd = fopen("{$g['vardb_path']}/voucher_{$cpzone}_active_$roll.db", "w");
506
	if ($fd) {
507
		foreach ($active as $voucher => $value) {
508
			fwrite($fd, "$voucher,$value\n");
509
		}
510
		fclose($fd);
511
	}
512
}
513

    
514
/* return how many vouchers are marked used on a roll */
515
function voucher_used_count($roll) {
516
	global $g, $cpzone;
517

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

    
531
	return $used;
532
}
533

    
534
function voucher_read_used_db($roll) {
535
	global $g, $cpzone;
536

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

    
551
function voucher_unlink_db($roll) {
552
	global $g, $cpzone;
553
	@unlink("{$g['vardb_path']}/voucher_{$cpzone}_used_$roll.db");
554
	@unlink("{$g['vardb_path']}/voucher_{$cpzone}_active_$roll.db");
555
}
556

    
557
/* we share the log with captiveportal for now */
558
function voucher_log($priority, $message) {
559

    
560
	$message = trim($message);
561
	openlog("logportalauth", LOG_PID, LOG_LOCAL4);
562
	syslog($priority, sprintf(gettext("Voucher: %s"), $message));
563
	closelog();
564
}
565

    
566
/* Perform natural expiration of vouchers
567
 * Called during reboot -> system_reboot_cleanup() and every active voucher change
568
 */
569
function voucher_save_db_to_config() {
570
	global $config, $g;
571

    
572
	if (is_array($config['voucher'])) {
573
		foreach ($config['voucher'] as $zone => $vcfg) {
574
			if (isset($config['voucher'][$zone]['enable']) && is_array($config['voucher'][$zone]['roll'])) {
575
				foreach ($config['voucher'][$zone]['roll'] as $key => $rollent) {
576
					// read_active_db will remove expired vouchers that are still active
577
					voucher_read_active_db($rollent['number']);
578
				}
579
			}
580
		}
581
	}
582
}
583

    
584
?>
(54-54/61)