Changeset 3334399
- Timestamp:
- 07/26/2025 12:33:24 AM (7 months ago)
- Location:
- bitfire/trunk
- Files:
-
- 14 edited
-
bitfire-admin.php (modified) (1 diff)
-
bitfire-plugin.php (modified) (1 diff)
-
readme.txt (modified) (2 diffs)
-
src/api.php (modified) (8 diffs)
-
src/bitfire.php (modified) (2 diffs)
-
src/botfilter.php (modified) (2 diffs)
-
src/dashboard.php (modified) (11 diffs)
-
src/server.php (modified) (20 diffs)
-
src/util.php (modified) (10 diffs)
-
src/webfilter.php (modified) (2 diffs)
-
startup.php (modified) (1 diff)
-
uninstall.php (modified) (2 diffs)
-
views/traffic.html (modified) (1 diff)
-
views/wizard.html (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
bitfire/trunk/bitfire-admin.php
r3057065 r3334399 17 17 use const BitFire\FILE_W; 18 18 use const BitFire\INFO; 19 use const BitFire\WAF_INI;20 19 use const BitFire\WAF_ROOT; 21 20 use const BitFire\WAF_SRC; -
bitfire/trunk/bitfire-plugin.php
r3250587 r3334399 24 24 * Description: Only RASP firewall for WordPress. Stop malware, redirects, back-doors and account takeover. 100% bot blocking, backups, malware cleaner. 25 25 * Description: Only RASP firewall for WordPress. Stop malware, redirects, back-doors and account takeover. 100% bot blocking, backups, malware cleaner. 26 * Version: 4. 526 * Version: 4.6 27 27 * Author: BitFire.co 28 28 * License: AGPL-3.0+ -
bitfire/trunk/readme.txt
r3250641 r3334399 5 5 Tags: security, firewall, malware scanner, waf, activity log 6 6 Requires at least: 5.0.0 7 Tested up to: 6. 7.28 Stable tag: 4. 5.07 Tested up to: 6.8.2 8 Stable tag: 4.6.0 9 9 Requires PHP: 7.4 10 10 License: AGPLv3 or later … … 220 220 221 221 == Changelog == 222 223 = 4.6 = 224 * Address a potential information disclosure issue on miss-configured web servers with WordPress core file changes. Early July WordFence 225 issued CVE-2025-6722 for BitFire. The WordFence team expressed concern that web servers with directory listings enabled 226 (a miss-configuration present on <1% of servers globally) AND had also deleted the WordPress core file wp-content/plugins/index.php 227 could expose a hidden directory containing filtered web log data for BitFire and firewall configuration settings. While this exact 228 setup requires multiple security misconfigurations to be present on an affected system - WordPress requested the plugin be 229 removed from the official WordPress plugin repository. After several weeks of discussion about possible resolutions the following 230 changes were accepted as mitigation. The upgrade will apply these changes automatically: 231 1. The configuration and log data was moved from /wp-content/plugins/bitfire_RANDOM to /wp-content/uploads/bitfire_RANDOM per the WordPress team 232 2. The length of the random directory name was increased from 10 characters to 12 characters. 233 3. A third method of protection was added with a .htaccess file to restrict access for web servers that support .htaccess files. 234 4. A new check was added to ensure that the file wp-content/uploads/index.php is always present to prevent directory listings 235 that could expose the path to this hidden directory. 222 236 223 237 = 4.5 = -
bitfire/trunk/src/api.php
r3250657 r3334399 52 52 use function ThreadFin\HTTP\httpp; 53 53 use function ThreadFin\icontains; 54 use function ThreadFin\make_config_loader; 54 55 use function ThreadFin\trace; 55 56 use function ThreadFin\ƒ_id; … … 541 542 $name = $request->post["param"]; 542 543 544 $config_file = make_config_loader()->run()->read_out(); 545 543 546 // remove all lines with $name[] 544 $file_no_array = FileData::new( WAF_INI)->read()->filter(function($line) use ($name) {547 $file_no_array = FileData::new($config_file)->read()->filter(function($line) use ($name) { 545 548 return ! contains($line, "{$name}[]"); 546 549 }); … … 556 559 557 560 // write the new file 558 $effect->file(new FileMod( WAF_INI, join("", $file_no_array->lines)));561 $effect->file(new FileMod($config_file, join("", $file_no_array->lines))); 559 562 560 563 // remove the old cache entry and force a new parse … … 719 722 file_recurse(\BitFire\WAF_ROOT."data/bitfire-{$v}", function (string $x) use ($v) { 720 723 $base = basename($x); 721 if (is_file($x) && $base != "config.ini") {724 if (is_file($x) && ends_with($x, "config.ini")) { 722 725 $root = str_replace(\BitFire\WAF_ROOT."data/bitfire-{$v}/", "", $x); 723 726 if (!rename($x, \BitFire\WAF_ROOT . $root)) { debug("unable to rename [%s] - %s", $x, $root); } … … 782 785 $p1 = hash("sha3-256", $request->post['pass1']??''); 783 786 debug("pass sha3-256 %s ", $p1); 784 $pass = file_replace(\BitFire\WAF_INI, "password = 'default'", "password = '$p1'")->run()->num_errors() == 0; 787 788 $config_file = make_config_loader()->run()->read_out(); 789 $pass = file_replace($config_file, "password = 'default'", "password = '$p1'")->run()->num_errors() == 0; 785 790 CacheStorage::get_instance()->save_data("parse_ini", null, -86400); 786 exit(($pass) ? "success" : "unable to write to: " . \BitFire\WAF_INI);791 exit(($pass) ? "success" : "unable to write to: $config_file"); 787 792 } 788 793 … … 850 855 function toggle_config_value(\BitFire\Request $request) : Effect { 851 856 857 $config_file = make_config_loader()->run()->read_out(); 852 858 // handle fixing write permissions 853 859 if ($request->post["param"] == "unlock_config") { 854 $result = chmod( \BitFire\WAF_INI, 0664);855 return Effect::new()->api(true, "updated 2", ["file" => WAF_INI, "mode" => 0664, "result" => $result]);860 $result = chmod($config_file, 0664); 861 return Effect::new()->api(true, "updated 2", ["file" => $config_file, "mode" => 0664, "result" => $result]); 856 862 } 857 863 // handle toggle on/off to values … … 861 867 } 862 868 863 debug("update config [%s]", WAF_INI); 864 865 // ugly fix for missing valid domain line 866 $config = FileData::new(WAF_INI)->read(); 867 868 if ($config->num_lines < 1) { 869 file_replace(WAF_INI, "; domain_fix_line", "valid_domains[] = \"\"\n; domain_fix_line")->run(); 870 } 869 debug("update config [%s]", $config_file); 871 870 872 871 // update the config file … … 1248 1247 $effect = Effect::new()->exit(true); 1249 1248 $weblog_file = get_hidden_file("weblog.bin"); 1250 $fh = fopen($weblog_file, "rb"); 1249 if (file_exists($weblog_file)) { 1250 $fh = fopen($weblog_file, "rb"); 1251 } 1251 1252 if (!$fh) { 1252 1253 return $effect->api(false, "unable to open $weblog_file"); -
bitfire/trunk/src/bitfire.php
r3250641 r3334399 264 264 } 265 265 266 $config_file = \ThreadFin\make_config_loader()->run()->read_out(); 266 267 $raw_pw = $_SERVER["PHP_AUTH_PW"]??''; 267 268 // read any recovery passwords … … 274 275 // set the password and unlock the config file 275 276 $password = trim(file_get_contents($file)); 276 @chmod( WAF_INI, FILE_RW);277 @chmod($config_file, FILE_RW); 277 278 } 278 279 } -
bitfire/trunk/src/botfilter.php
r3212335 r3334399 1257 1257 1258 1258 // create a new bot if we could not load one from the remote server (bitfire.co down?) 1259 if ( !empty($request) &&empty($bot_data)) {1259 if (empty($bot_data)) { 1260 1260 trace('BOT_NEW'); 1261 1261 $bot_data = new BotSimpleInfo($agent->trim); 1262 1262 $bot_data->agent_trim = $agent->trim; 1263 $bot_data->ips = [$request->ip => $request->classification]; 1263 if (!empty($request)) { 1264 $bot_data->ips = [$request->ip => $request->classification]; 1265 } 1264 1266 $bot_data->category = 'Auto Learn'; 1265 1267 $bot_data->name = ''; … … 1277 1279 1278 1280 // bot_data will now have known bots and unknown bots, manual mode will be set 1279 $bot_data->agent = $agent->agent_text; 1281 if (empty($bot_data->agent)) { 1282 $bot_data->agent = $agent->agent_text; 1283 } 1280 1284 1281 1285 -
bitfire/trunk/src/dashboard.php
r3250641 r3334399 68 68 use function ThreadFin\un_json; 69 69 use function ThreadFin\at; 70 use function ThreadFin\make_config_loader; 70 71 71 72 require_once \BitFire\WAF_SRC . "api.php"; … … 170 171 { 171 172 static $result = NULL; 173 $config_file = make_config_loader()->run()->read_out(); 172 174 if ($result === NULL) { 173 $result = is_writeable( \BitFire\WAF_INI) && is_writeable(\BitFire\WAF_ROOT . "config.ini.php");175 $result = is_writeable($config_file) && is_writeable(\BitFire\WAF_ROOT . "config.ini.php"); 174 176 } 175 177 return ($result) ? " " : "disabled "; … … 233 235 234 236 $content = CFG::str("cms_content_url"); 237 $is_default = CFG::enabled('default_config'); 235 238 $variables['license'] = CFG::str('pro_key', "unlicensed"); 236 239 $variables['font_path'] = (defined("WPINC") && !empty($content)) ? "$content/plugins/bitfire/public" : "https://bitfire.co/dash/fonts/cerebrisans"; … … 250 253 $variables['sym_version'] = BITFIRE_SYM_VER; 251 254 $variables['showfree_class'] = $is_free ? "" : "hidden"; 255 $variables['showerror_class'] = $is_default ? "" : "hidden"; 256 252 257 $variables['hidefree_class'] = $is_free ? "hidden" : ""; 253 258 $variables['release'] = (($is_free) ? "FREE" : "PRO") . " Release " . BITFIRE_SYM_VER; … … 313 318 if ($auth->read_status() == 302) { return; } 314 319 320 $config_file = make_config_loader()->run()->read_out(); 315 321 // load the scanner config 316 322 $raw_scan_config = CFG::arr("malware_config"); 317 323 if (empty($raw_scan_config)) { 318 324 $raw_scan_config = ["unknown_core:1", "standard_scan:false", "access_time:1", "random_name_per:50", "line_limit:12000", "freq_limit:768", "random_name_per:75", "fn_freq_limit:512", "fn_line_limit:2048", "fn_random_name_per:60", "includes:0", "var_fn:1", "call_func:1", "wp_func:0", "extra_regex:"]; 319 $eff = update_ini_fn(ƒixl('\BitFireSvr\array_to_ini', 'malware_config', $raw_scan_config), WAF_INI, true);325 $eff = update_ini_fn(ƒixl('\BitFireSvr\array_to_ini', 'malware_config', $raw_scan_config), $config_file, true); 320 326 $eff->run(); 321 327 } … … 419 425 function serve_settings() 420 426 { 427 $ini_file = make_config_loader()->run()->read_out(); 428 421 429 // authentication guard 422 430 $auth = validate_auth(); … … 448 456 449 457 $policy = CFG::arr("csp_policy"); 458 450 459 451 460 //"dashboard_path" => $dashboard_path, … … 463 472 "cor_policy" => (CFG::str("cor_policy") == "same-site") ? true : false, 464 473 //"theme_css" => file_get_contents(\BitFire\WAF_ROOT."public/theme.min.css"). file_get_contents(\BitFire\WAF_ROOT."public/theme.bundle.css"), 465 "valid_domains_html" => list_text_inputs("valid_domains"),466 474 "hide_shmop" => (function_exists("shmop_open")) ? "" : "hidden", 467 475 "hide_apcu" => (function_exists("apcu_store")) ? "" : "hidden", … … 472 480 "disabled" => $disabled, 473 481 "info" => $info, 474 "waf_ini" => WAF_INI,482 "waf_ini" => $ini_file, 475 483 "mfa_class" => (defined("WPINC")) ? "text-muted" : "text-danger" 476 484 )))->run(); … … 624 632 $bot = hydrate_any_bot_file($file); 625 633 626 if ( is_array($bot->ips)) {634 if (!empty($bot) && is_array($bot->ips)) { 627 635 foreach ($bot->ips as $ip => $unused_class) { 628 636 $ip_counter[$ip] = ($ip_counter[$ip] ?? 0) + 1; … … 632 640 633 641 634 if (!$bot ) {642 if (!$bot && file_exists($file)) { 635 643 unlink($file); 636 644 return false; -
bitfire/trunk/src/server.php
r3212330 r3334399 45 45 public $crawler_id; // udger code, to remove from old bot data... 46 46 47 public $manual_mode; 48 47 49 public function __construct($agent) 48 50 { … … 86 88 use const BitFire\STATUS_OK; 87 89 use const BitFire\STATUS_FAIL; 88 use const BitFire\WAF_INI;89 90 use const BitFire\WAF_ROOT; 90 91 use const BitFire\WAF_SRC; 92 use const ThreadFin\CUCKOO_MEM_CHUNK; 93 use const ThreadFin\CUCKOO_STAT_SIZE; 91 94 use const ThreadFin\DAY; 92 95 use const ThreadFin\DS; 93 94 95 96 96 97 use function BitFire\parse_agent; … … 118 119 use function ThreadFin\trace; 119 120 use function ThreadFin\at; 121 use function ThreadFin\make_ini_info; 120 122 use function ThreadFin\utc_date; 121 123 use function ThreadFin\utc_time; … … 132 134 const ACCESS_URL_URI = 13; 133 135 134 const CONFIG_KEY_NAMES = [ "bitfire_enabled","allow_ip_block","security_headers_enabled","enforce_ssl_1year","csp_policy_enabled","csp_default","csp_policy","csp_uri","pro_key","rasp_filesystem","max_cache_age","web_filter_enabled","spam_filter_enabled","xss_block","sql_block","file_block","block_profanity","filtered_logging","allowed_methods","whitelist_enable","blacklist_enable","require_full_browser","honeypot_url","check_domain","valid_domains","valid_domains[]","ignore_bot_urls","rate_limit","rr_5m","cache_type","cookies_enabled","wordfence_emulation","report_file","block_file","debug_file","debug_header","send_errors","dashboard_usage","browser_cookie","dashboard_path","encryption_key","secret","password","cms_root","cms_content_url","cms_content_dir","debug","skip_local_bots","response_code","ip_header","dns_service","short_block_time","medium_block_time","long_block_time","cache_ini_files","root_restrict","configured","log_everything","ip_lookups","remote_tech_allow","tech_public_key","nag_ignore","verify_http_code", "block_profanity"]; 135 136 const CONFIG_KEY_NAMES = [ "bitfire_enabled","allow_ip_block","security_headers_enabled","enforce_ssl_1year","csp_policy_enabled","csp_default","csp_policy","csp_uri","pro_key","rasp_filesystem","max_cache_age","web_filter_enabled","spam_filter_enabled","xss_block","sql_block","file_block","block_profanity","filtered_logging","allowed_methods","whitelist_enable","blacklist_enable","require_full_browser","honeypot_url","check_domain","ignore_bot_urls","rate_limit","rr_5m","cache_type","cookies_enabled","wordfence_emulation","report_file","block_file","debug_file","debug_header","send_errors","dashboard_usage","browser_cookie","dashboard_path","encryption_key","secret","password","cms_root","cms_content_url","cms_content_dir","debug","skip_local_bots","response_code","ip_header","dns_service","short_block_time","medium_block_time","long_block_time","cache_ini_files","root_restrict","configured","log_everything","ip_lookups","remote_tech_allow","tech_public_key","nag_ignore","verify_http_code", "block_profanity"]; 136 137 137 138 // helpers … … 334 335 function update_ini_fn(callable $fn, string $filename = "", bool $append = false) : Effect { 335 336 if (empty($filename)) { 336 $filename = (defined("\BitFire\WAF_INI")) ? \BitFire\WAF_INI : make_config_loader()->run()->read_out(); 337 $filename = make_config_loader()->run()->read_out(); 338 // file if we can not find it... 339 if (empty($filename) || !file_exists($filename)) { 340 return new Effect(); 341 } 337 342 } 338 343 … … 421 426 $fn = (ƒixl("preg_replace", $search, $replace)); 422 427 428 $filename = make_config_loader()->run()->read_out(); 429 423 430 // replace the parameter 424 if (icontains(FileData::new( WAF_INI)->read()->raw(), "$param")) {425 $effect = update_ini_fn($fn, WAF_INI);431 if (icontains(FileData::new($filename)->read()->raw(), "$param")) { 432 $effect = update_ini_fn($fn, $filename); 426 433 } 427 434 // append the parameter 428 435 else { 429 $effect = update_ini_fn(ƒ_id($replace), WAF_INI, true);436 $effect = update_ini_fn(ƒ_id($replace), $filename, true); 430 437 } 431 438 … … 470 477 } 471 478 479 $config_file = make_config_loader()->run()->read_out(); 480 if (empty($config_file)) { 481 return Effect::$NULL; 482 } 483 472 484 // if we already have the content, skip 473 $content = file_get_contents(WAF_INI); 485 if (file_exists($config_file)) { 486 $content = file_get_contents($config_file); 487 } 488 if (empty($content)) { 489 return Effect::$NULL; 490 } 474 491 if (contains($content, $param)) { 475 492 return Effect::$NULL; … … 489 506 $line = preg_replace("/^\n\n/", "\n", trim($line)); 490 507 491 return Effect::new()->file(new FileMod( WAF_INI, $content . $line));508 return Effect::new()->file(new FileMod($config_file, $content . $line)); 492 509 } 493 510 … … 508 525 $ini_test = FileData::new($ini_src); 509 526 // FILESYSTEM GUARDS 510 if (! $ini_test->exists) { return $e->exit(false, STATUS_EEXIST, " $ini_src does not exist!"); }527 if (! $ini_test->exists) { return $e->exit(false, STATUS_EEXIST, "config file $ini_src does not exist!"); } 511 528 if (! $ini_test->readable || ! $ini_test->writeable) { 512 529 if (!@chmod($ini_src, FILE_RW)) { … … 630 647 $info["cookies"] = "not enabled. none found. <= 1"; 631 648 } 632 633 $host = filter_input(INPUT_SERVER, "HTTP_HOST", FILTER_SANITIZE_URL);634 $domain = at($host, ":", 0);635 $info["domain_value"] = $domain;636 $domain = join(".", array_slice(explode(".", $domain), -2));637 638 $e->chain(update_ini_value("valid_domains[]", $domain, "default"));639 649 640 650 // configure dynamic exceptions … … 816 826 } 817 827 $ini = "$root/".ini_get("user_ini.filename"); 818 $hta = "$root/.htaccess";819 828 $extra = ""; 820 829 $note = ""; 821 $status = false; 830 $status = STATUS_FAIL; 831 832 // make sure the config has been setup! 833 $config_file = make_config_loader()->run()->read_out(); 822 834 823 835 … … 828 840 $ip = filter_input(INPUT_SERVER, CFG::str_up("ip_header", "REMOTE_ADDR"), FILTER_VALIDATE_IP); 829 841 $block_file = \BitFire\BLOCK_DIR . DS . $ip; 830 $effect->chain(update_config( \BitFire\WAF_INI));842 $effect->chain(update_config($config_file)); 831 843 $effect->chain(update_ini_value("configured", "true")); // MUST SYNC WITH UPDATE_CONFIG CALLS (WP) 832 844 $effect->chain(Effect::new()->file(new FileMod(\BitFire\WAF_ROOT."install.log", "configured server settings. rare condition.", FILE_RW, 0, true))); … … 843 855 844 856 // force WordFence compatibility mode if running on WP ENGINE and WordFence is not installed, emulate WordFence 857 // WPEngine only allows creation of the root file wordfence-waf.php, all others will be blocked. 845 858 // don't run this check if we are being run from the activation page (request will be null) 846 if (CFG::enabled("wordfence_emulation")) { 847 $cms_root = cms_root(); 848 $waf_load = "$cms_root/wordfence-waf.php"; 849 $effect->exit(false, STATUS_EEXIST, "WPEngine hosting. UNINSTALL WordFence before enabling always on."); 850 // we are on wordpress, found the dir and it exists 851 if (!empty($cms_root) && file_exists($cms_root)) { 852 // wordfence is not installed, and the autoload file does not exist, lets inject ours 853 if (!file_exists(CFG::str("cms_content_dir")."plugins/wordfence") && !file_exists($waf_load)) { 854 $self = dirname(__DIR__) . "/startup.php"; 855 if (file_exists($self)) { 856 $effect->file(new FileMod($waf_load, "<?"."php include_once '$self'; ?>\n")) 857 ->status(STATUS_OK) 858 ->out("WPEngine hosting. WordFence WAF emulation enabled. Always on protected."); 859 } else { 860 $effect->exit(false, STATUS_ENOENT, "Critical error, unable to locate BitFire startup script. Please re-install."); 861 } 862 } 859 // wp-content located: check on the boot strap file 860 $waf_load_file = CFG::enabled("wordfence_emulation") ? "$root/wordfence-waf.php" : "$root/bitfire-waf.php"; 861 $extra = "This may take up to " . ini_get("user_ini.cache_ttl") . " seconds to take effect (cache clear time)"; 862 863 // boot strap exists, lets verify it's content 864 if (file_exists($waf_load_file)) { 865 $content = file_get_contents($waf_load_file); 866 if (\str_contains(strtolower($content), "wordfence")) { 867 $note = "WordFence WAF already installed at $waf_load_file. Please uninstall WordFence before enabling BitFire always on protection."; 868 } 869 else if (! \str_contains(strtolower($content), "bitfire")) { 870 $note = "Unknown content in $waf_load_file. manually remove this file from " . $_SERVER['DOCUMENT_ROOT'] . "/.user.ini FIRST, THEN SECOND remove $waf_load_file and retry. Consult support at [email protected] for help, this can damage your web site."; 863 871 } else { 864 $effect->exit(false, STATUS_ENOENT, "Critical error, unable to locate WordPress root directory."); 865 } 866 } 867 868 // NOT WPE 872 $status = STATUS_OK; 873 } 874 } 875 // make the new boot strap file 869 876 else { 870 // handle NGINX and other cases 871 $root_path = dirname(__DIR__) . DS; 872 $content = "\nauto_prepend_file = \"{$root_path}startup.php\"\n"; 873 $status = (\BitFireSvr\install_file($ini, $content) ? true : false); 874 $file = $ini; 875 $extra = "This may take up to " . ini_get("user_ini.cache_ttl") . " seconds to take effect (cache clear time)"; 876 $note = ($status == "success") ? 877 "BitFire was added to auto start in [$ini]. $extra" : 878 "Unable to add BitFire to auto start. check permissions on file [$file]"; 879 } 880 881 $effect->chain(Effect::new()->file(new FileMod(\BitFire\WAF_ROOT."install.log", join(", ", debug(null))."\n$note\n", FILE_RW, 0, true))); 877 $full_path = realpath(WAF_ROOT . "startup.php"); 878 // only make the bootstrap file if we can find the plugin startup file 879 if ($full_path) { 880 $content = "<?php\n// THIS FILE LOADS BITFIRE FROM .user.ini NO NOT REMOVE!\nif (file_exists($full_path)) { include_once '$full_path'; } ?>\n"; 881 $effect->file(new FileMod($waf_load_file, $content)); 882 $effect->run(); 883 $status = $effect->num_errors() == 0 ? STATUS_OK : STATUS_FAIL; 884 } else { 885 $note = "Critical error, unable to locate BitFire startup script. Please re-install."; 886 } 887 } 888 889 // bootstrap is looking good and was written successfully! 890 if ($status == STATUS_OK && empty($effect->read_errors())) { 891 $note = "BitFire always on protection installed. DO NOT MANUALLY REMOVE THE $waf_load_file FILE! Contact support at [email protected] for support\n"; 892 $ini_content = "\nauto_prepend_file = \"{$root_path}startup.php\"\n"; 893 $status = (\BitFireSvr\install_file($ini, $ini_content) ? STATUS_OK : STATUS_EACCES); 894 } 895 896 $effect->chain(Effect::new()->file(new FileMod(get_hidden_file("install.log"), join(", ", debug(null))."\n$note\n", FILE_RW, 0, true))); 882 897 return $effect->exit(false)->api($status, $note)->status((($status) ? STATUS_OK : STATUS_FAIL)); 883 898 } … … 902 917 $sem = sem_get(0x228AAAE7, 1, 0660, $opt); 903 918 if (!empty($sem)) { sem_remove($sem); } 919 } 920 // remove any dangling shmop cache 921 if (function_exists('shmop_delete')) { 922 $token = Config::int("cache_token", 1234560); 923 $mem_end = ((4096 + 1) * CUCKOO_MEM_CHUNK) + CUCKOO_STAT_SIZE; 924 $memory = shmop_open($token, "w", 0644, $mem_end); 925 if (!empty($memory)) { 926 shmop_delete($memory); 927 } 904 928 } 905 929 … … 938 962 // remove all configuration... 939 963 do_for_each(glob(get_hidden_file("*"), GLOB_NOSORT), [$effect, 'unlink']); 940 $effect->unlink(get_hidden_file("")); 964 do_for_each(glob(get_hidden_file("bots", GLOB_NOSORT)), [$effect, 'unlink']); 965 do_for_each(glob(get_hidden_file("params", GLOB_NOSORT)), [$effect, 'unlink']); 966 do_for_each(glob(get_hidden_file("quick_map", GLOB_NOSORT)), [$effect, 'unlink']); 967 $config_dir_path = get_hidden_file(""); 968 $effect->unlink($config_dir_path); 941 969 942 970 $note = ($status == "success") ? … … 946 974 $effect->out(json_encode(array('status' => $status, 'note' => $note, 'method' => $method, 'path' => $path))); 947 975 976 file_put_contents("/tmp/uninstall.log", print_r($effect, true)); 948 977 949 978 return $effect; … … 1099 1128 } 1100 1129 1130 // make sure the config has been setup! 1131 $config_file = make_config_loader()->run()->read_out(); 1132 1101 1133 $effect = \BitFireSvr\update_ini_value("bitfire_enabled", "true"); 1102 1134 debug("configured: [%d]", CFG::enabled("configured")); 1103 1135 1104 $effect->chain(update_config( \BitFire\WAF_INI));1136 $effect->chain(update_config($config_file)); 1105 1137 // make sure we run auto configure and install auto start 1106 1138 // update configured after check for install. allows install on deactivate - activate … … 1246 1278 } 1247 1279 1280 /** 1281 * 1282 * perform a basic check to make sure that the config directory is looking good. 1283 * @param string $path 1284 * @return bool - true if the config is okay, false if not 1285 */ 1286 function verify_config(string $path) : bool { 1287 $files = glob($path . "/*"); 1288 if (count($files) < 11) { 1289 return false; 1290 } 1291 1292 $have_config = $have_bots = false; 1293 foreach($files as $file) { 1294 if (ends_with($file, "config.ini")) { 1295 $have_config = true; 1296 } 1297 if (ends_with($file, "bots")) { 1298 $have_bots = true; 1299 } 1300 } 1301 1302 return $have_config && $have_bots; 1303 } 1304 1305 /** 1306 * Migrate the configuration directory to the new location. 1307 * @param string $orig_key - the old key - if empty, a new key will be generated 1308 * @return string - the new key if migration passed, empty string on failure 1309 */ 1310 function migrate_config_dir(string $orig_key) : string { 1311 // create a new key if we are using the default key 1312 static $random_key = ''; 1313 if (empty($random_key)) { $random_key = random_str(12); } 1314 1315 $new_key = (empty($orig_key)) ? $random_key : $orig_key; 1316 1317 // this is already defined in wp-includes/load.php, I'm just paranoid 1318 if (defined('ABSPATH') && !defined('WP_CONTENT_DIR')) { define('WP_CONTENT_DIR', ABSPATH . 'wp-content'); } 1319 1320 // don't allow a double migration 1321 $migration_path = WP_CONTENT_DIR . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . "bitfire_$new_key"; 1322 if (file_exists($migration_path) && is_dir($migration_path)) { 1323 // if the migration path already exists, we are done 1324 return $new_key; 1325 } 1326 1327 $old_path = $old_key = ''; 1328 // look in the old location for the config directory 1329 $old_configs = glob(WP_CONTENT_DIR . "/plugins/bitfire_??????????"); 1330 // found an old config in the old location, let's move it. 1331 if (count($old_configs) > 0) { 1332 // we have an old config, so we will migrate it 1333 $old_path = realpath($old_configs[0]); 1334 if ($old_path && verify_config($old_path)) { 1335 // the old config is good, we are going to use it, lets move the 1336 // hidden_config directory so we don't accidentally use that some other time 1337 rename(WAF_ROOT . "hidden_config", WAF_ROOT . "orig_config"); 1338 $tmp = basename($old_path); 1339 $parts = explode("_", $tmp); 1340 $old_key = $parts[1]; 1341 } 1342 } 1343 1344 // next, look for an "OLD" config in the NEW location... 1345 $old_configs = glob(WP_CONTENT_DIR . "/uploads/bitfire_????????????"); 1346 // found an old config in the old location, let's move it. 1347 if (count($old_configs) > 0) { 1348 // we have an old config, so we will migrate it 1349 $old_path = realpath($old_configs[0]); 1350 if ($old_path && verify_config($old_path)) { 1351 // the old config is good, we are going to use it, lets move the 1352 // hidden_config directory so we don't accidentally use that some other time 1353 rename(WAF_ROOT . "hidden_config", WAF_ROOT . "orig_config"); 1354 $tmp = basename($old_path); 1355 $parts = explode("_", $tmp); 1356 $old_key = $parts[1]; 1357 } 1358 } 1359 1360 // no valid old config found, look for the default config (could be a new install) 1361 if (empty($old_path) || empty($old_key)) { 1362 $default_path = WP_CONTENT_DIR . "/plugins/bitfire/hidden_config"; 1363 // we still have an old config, so we will migrate it 1364 if (file_exists($default_path)) { 1365 $old_path = realpath($default_path); 1366 } 1367 } 1368 1369 $success = false; 1370 // move the old configuration to the new configuration directory 1371 if ($old_path && file_exists($old_path) && is_dir($old_path)) { 1372 $success = rename($old_path, $migration_path); 1373 if ($success) { 1374 $effect = make_ini_info($new_key)->run(); 1375 1376 if ($effect->num_errors() <= 0) { 1377 return $new_key; 1378 } 1379 } 1380 } 1381 1382 // looks like ini_info wasn't updated correctly (stat cache?). try to find the already migrated config... 1383 $old_configs = glob(WP_CONTENT_DIR . DIRECTORY_SEPARATOR . "uploads/bitfire_????????????", GLOB_ONLYDIR); 1384 if ($old_configs) { 1385 $config = array_shift($old_configs); 1386 $secret = substr(basename($config), -12); 1387 $effect = make_ini_info($secret)->run(); 1388 return $secret; 1389 } 1390 1391 return ''; 1392 } 1248 1393 1249 1394 function upgrade($upgrade=null, $extra=null) { … … 1339 1484 update_ini_value("tech_public_key", "b39a09eb3095c54fd346a2f3c8a13a8f143a1b3fe26b49c286389c55cec73c3e")->run(); 1340 1485 1341 file_put_contents(dirname(WAF_INI)."/browser_allow.json", "{\n'ip': {},\n'ua': {}\n }", LOCK_EX); 1486 $browser_allow_file = get_hidden_file("browser_allow.json"); 1487 if (!file_exists($browser_allow_file)) { 1488 file_put_contents($browser_allow_file, "{\n'ip': {},\n'ua': {}\n }", LOCK_EX); 1489 } 1342 1490 1343 1491 // convert old format bots to BotSimpleInfo … … 1663 1811 1664 1812 // write to a temp file and make sure it loads 1665 file_put_contents($config_file . ".tmp", $file->raw() . "\n"); 1666 $success = parse_ini_file($config_file . ".tmp"); 1667 // there is an error, return the mapped config data, will try again later 1668 if ($success == false) { 1669 return $data; 1813 $raw = $file->raw(); 1814 $success = false; 1815 $raw_len = strlen($raw); 1816 if ($raw_len > 1024) { 1817 $bytes = file_put_contents($config_file . ".tmp", $raw . "\n"); 1818 if ($bytes < $raw_len) { 1819 $success = parse_ini_file($config_file . ".tmp"); 1820 // there is an error, return the mapped config data, will try again later 1821 if ($success == false) { 1822 return $data; 1823 } 1824 } 1670 1825 } 1671 1826 -
bitfire/trunk/src/util.php
r3250641 r3334399 34 34 use function BitFire\on_err; 35 35 use function BitFireChars\save_config2; 36 use function BitFirePlugin\file_type; 36 37 use function BitFireSvr\update_ini_value; 37 38 use function ThreadFin\HTTP\http; … … 386 387 static $last = 0; if (is_null($msg)) { return $last; } 387 388 $last = microtime(true); trace($msg); 389 } 390 // emergency logger in case config can not load 391 function emerg(string $msg) :void { 392 file_put_contents("/tmp/bitfire_emerg.log", date('Y-m-d H:i:s') . " " . $msg . "\n", FILE_APPEND); 388 393 } 389 394 function dbg($x, $msg="") {$m=htmlspecialchars($msg); $z=(php_sapi_name() == "cli") ? print_r($x, true) : htmlspecialchars(print_r($x, true)); echo "<pre>\n[$m]\n($z)\n" . join("\n", debug(null)) . "\n" . debug(trace(null)); … … 968 973 // write all effect files 969 974 foreach ($this->file_outs as $file) { 975 970 976 assert(!empty($file->filename), "can't write to null file: " . en_json($file)); 971 977 $len = strlen($file->content); … … 1014 1020 } 1015 1021 } 1016 } 1022 1023 // we need to clear the entry from the stat cache so we can read the updated file 1024 clearstatcache(true, $file->filename); 1025 } 1026 1017 1027 1018 1028 // allowable: backup files, WordFence waf loader if it is an emulation file … … 1575 1585 function make_config_loader() : Effect { 1576 1586 $effect = Effect::new(); 1577 if (defined("BitFire\WAF_INI")) { return $effect->out(\BitFire\WAF_INI)->hide_output(); } 1578 1579 1580 // FIRST, lets verify that we already have a valid config 1581 // if so we bail out early here... 1582 $parent = dirname(WAF_ROOT, 1); 1587 static $path = ""; 1588 if (!empty($path)) { return $effect->out($path)->hide_output(); } 1589 1590 1591 // normal path, load the secret key are return the path to the config file 1583 1592 $file = \BitFire\WAF_ROOT."ini_info.php"; 1593 $secret_key = ""; 1584 1594 if (file_exists($file)) { 1585 $secret_key = "";1595 // including the ini_info.php file will define $secret_key and $ini_type 1586 1596 include $file; 1587 $config_file = $parent . "/bitfire_{$secret_key}/config.ini"; 1588 if (file_exists($config_file)) { 1589 define("BitFire\WAF_INI", $config_file); 1597 $config_path = WP_CONTENT_DIR . DIRECTORY_SEPARATOR . "uploads/bitfire_{$secret_key}/config.ini"; 1598 $config_file = realpath($config_path); 1599 // normal case after secret directory move 1600 if ($config_file) { 1601 $path = $config_file; 1590 1602 return $effect->out($config_file)->hide_output(); 1591 1603 } 1592 1604 } 1593 // ini_info was invalid, reset the key 1594 $secret_key = ""; 1595 1596 // we don't know where the config is because there is no ini_info file 1597 // probably a first run, or a new install, lets find it 1598 // find all old configs 1599 $config_dirs = glob("{$parent}/bitfire_??????????"); 1600 1601 // get the creation/modification time so we can find most recent 1602 $dir_with_time = array_map(function($dir) { 1603 return [ "dir" => $dir, "time" => filemtime($dir) ]; 1604 }, $config_dirs); 1605 usort($dir_with_time, function($a, $b) { 1606 return $a["time"] - $b["time"]; 1607 }); 1608 // if we have existing dirs, then lets use the most recent config 1609 if (count($dir_with_time) > 0) { 1610 $newest = array_pop($dir_with_time); 1611 if (preg_match("/bitfire_(\w+)/", $newest["dir"], $matches)) { 1612 $secret_key = $matches[1]; 1613 while($next = array_pop($dir_with_time)) { 1614 // delete all but the newest 1615 $effect->unlink($next["dir"]); 1616 } 1617 } 1618 } 1619 // no old configs, lets create a new one 1620 if (empty($secret_key)) { 1621 $secret_key = random_str(10); 1622 // check if the hidden config has not yet been moved and move it 1623 $path = $parent . "/bitfire_{$secret_key}/"; 1624 $orig_config = WAF_ROOT . "hidden_config"; 1625 if (file_exists($orig_config)) { 1626 rename($orig_config, $path); 1627 } 1628 } 1629 1630 // we should have a secret key by now, lets update the ini_info file 1631 if (!empty($secret_key)) { 1632 $markup = "<?"."php \$secret_key = '$secret_key'; "; 1605 1606 // make sure we have the server modification functions available and migrate an old config 1607 require_once WAF_ROOT . "/src/server.php"; 1608 $new_key = \BitFireSvr\migrate_config_dir($secret_key); 1609 $config_path = WP_CONTENT_DIR . DIRECTORY_SEPARATOR . "uploads/bitfire_{$new_key}/config.ini"; 1610 1611 // we should have a valid config at this location now 1612 $config_file = realpath($config_path); 1613 1614 // normal case after secret directory move 1615 if (file_exists($config_file)) { 1616 $path = $config_file; 1617 return $effect->out($config_file)->hide_output(); 1618 } 1619 1620 // something has gone very wrong at this point. (probably file permissions) 1621 // return the default config file 1622 $orig_config = WAF_ROOT . "hidden_config"; 1623 return $effect->out($orig_config)->hide_output(); 1624 } 1625 1626 /** 1627 * create an effect to write the ini_info.php file, will auto determine the cache type if one is not given 1628 * @param string $secret_key 1629 * @param string $ini_type 1630 * @return Effect 1631 */ 1632 function make_ini_info(string $secret_key, string $ini_type = '') : Effect { 1633 $effect = Effect::new(); 1634 // attempt to use shared memory if available 1635 if (empty($ini_type)) { 1633 1636 if (function_exists("shmop_open")) { 1634 $markup .= '$ini_type = "shmop";'; 1635 } else { 1636 $markup .= '$ini_type = "opcache";'; 1637 } 1638 $effect->file(new FileMod(\BitFire\WAF_ROOT."ini_info.php", $markup)); 1639 } 1640 1641 $path = $parent . "/bitfire_{$secret_key}/"; 1642 define("BitFire\WAF_INI", $path . "config.ini"); 1643 return $effect->out($path . "config.ini")->hide_output(); 1644 } 1637 $id = @shmop_open(12345, 'c', 0666, 1470008); 1638 if (!empty($id)) { 1639 @shmop_delete($id); 1640 $ini_type = "shmop"; 1641 } 1642 } 1643 } 1644 // if we don't have shmop, then we use opcache 1645 if (empty($ini_type)) { 1646 $ini_type = "opcache"; 1647 } 1648 // if we have a secret key, then we write the ini_info.php file 1649 $markup = "<?php \$secret_key = '$secret_key'; \$ini_type = '$ini_type';"; 1650 $effect->file(new FileMod(\BitFire\WAF_ROOT."ini_info.php", $markup)); 1651 return $effect; 1652 } 1653 1645 1654 1646 1655 … … 1652 1661 * @return string - the realpath to the file 1653 1662 */ 1654 function get_hidden_file(string $file_name , ?string $secret_key = null) : string {1663 function get_hidden_file(string $file_name) : string { 1655 1664 static $path = null; 1656 1665 //if (php_sapi_name() === "cli") { return getcwd() . "/$file_name"; } 1657 1666 1658 // use the secret key passed to us1659 if (!empty($secret_key)) {1660 $parent = dirname(WAF_ROOT, 1);1661 $path = realpath($parent . "/bitfire_{$secret_key}/") . "/";1662 }1663 1667 // fall back to the secret key in the ini_info file 1664 1668 if (empty($path)) { 1665 $path = dirname(make_config_loader()->read_out(), 1) . "/"; 1666 } 1667 return $path . $file_name; 1669 // make config loader returns the path to the config file 1670 $config_file = make_config_loader()->run()->read_out(); 1671 // get the path to the hidden directory 1672 $test = dirname($config_file); 1673 // we found the config, save that path in the static variable for future use 1674 if (file_exists($test) && WAF_ROOT != $test) { 1675 $path = $test . DIRECTORY_SEPARATOR; 1676 } 1677 1678 $config_parent = dirname($test) . DIRECTORY_SEPARATOR; 1679 // make sure that the index.php file exists in the wp-content/uploads directory to hide the configuration 1680 if ($config_parent . "index.php") { 1681 file_put_contents($config_parent . "index.php", "<?php\n//Silence is golden.", LOCK_EX); 1682 } 1683 1684 } 1685 // very odd case (SHOULD NEVER HAPPEN) where the config file is not found, we use the default path 1686 if (empty($path)) { 1687 $path = WAF_ROOT; 1688 } 1689 // if we are requesting an empty file name, we just return the path (since they are requesting the hidden directory path) 1690 if ($file_name == "") { 1691 return $path; 1692 } 1693 1694 $file_path = $path . $file_name; 1695 return $file_path; // hold on to your butts! 1696 } 1697 1698 /** 1699 * a fail safe fall back config if we can't find one 1700 * @return array 1701 */ 1702 function default_config() : array { 1703 return [ 1704 "bitfire_enabled" => false, 1705 "allow_ip_block" => false, 1706 "security_headers_enabled" => false, 1707 "log_everything" => false, 1708 "web_filter_enabled" => false, 1709 "require_full_browser" => false, 1710 "whitelist_enable" => false, 1711 "blacklist_enable" => false, 1712 "cache_type" => "nop", 1713 "ip_header" => "remote_addr", 1714 "default_config" => true, 1715 "wizard" => true 1716 ]; 1668 1717 } 1669 1718 … … 1676 1725 //$ini_type = "opcache"; 1677 1726 1678 $loader = make_config_loader()->run(); 1679 $config_file = $loader->read_out(); 1727 1728 1729 $config_file = make_config_loader()->run()->read_out(); 1680 1730 $cache_config_file = $config_file . ".php"; 1681 1731 1682 1732 // return a core config with everything off if the config file is not found... 1683 1733 if (!file_exists($config_file)) { 1684 return [ 1685 "bitfire_enabled" => false, 1686 "allow_ip_block" => false, 1687 "security_headers_enabled" => false, 1688 "log_everything" => false, 1689 "web_filter_enabled" => false, 1690 "require_full_browser" => false, 1691 "whitelist_enable" => false, 1692 "blacklist_enable" => false, 1693 "cache_type" => "nop", 1694 "ip_header" => "remote_addr", 1695 "wizard" => false 1696 ]; 1734 return default_config(); 1697 1735 } 1698 1736 … … 1704 1742 if (!file_exists($cache_config_file) || filemtime($cache_config_file) < $mod_time) { 1705 1743 1706 $config = parse_ini_file($config_file, false, INI_SCANNER_TYPED); 1707 //$c = count($config); 1708 //die("config [$c]\n"); 1744 // support for json configs... 1745 if (strpos($config_file, ".json") !== false) { 1746 $config = json_decode(file_get_contents($config_file), true); 1747 } else { 1748 $config = parse_ini_file($config_file, false, INI_SCANNER_TYPED); 1749 } 1750 1751 // write the php cached config file 1709 1752 if (is_array($config) && count($config) > 20) { 1710 1753 // ensure that passwords are always hashed … … 1720 1763 $exp = time() + 86400*7; 1721 1764 $data = "<?php \$value = $s; \$priority = $priority; \$success = (time() < $exp);"; 1722 file_put_contents($cache_config_file, $data, LOCK_EX) == strlen($data); 1723 } 1765 file_put_contents($cache_config_file, $data, LOCK_EX); 1766 } 1767 // TODO: we need to add a notification that the config file is invalid 1724 1768 else { 1725 require_once WAF_SRC . "server.php"; 1726 $config = save_config2($config_file); 1769 return default_config(); 1727 1770 } 1728 1771 } … … 1730 1773 if (file_exists($cache_config_file)) { 1731 1774 include $cache_config_file; 1775 // normal case, we have a valid config 1732 1776 if (isset($value) && count($value) > 20) { 1733 1777 $config = $value; -
bitfire/trunk/src/webfilter.php
r3250587 r3334399 106 106 107 107 // update keys and values 108 $key_file = \BitFire\WAF_ROOT."data/keys2.raw"; 109 $value_file = \BitFire\WAF_ROOT."data/values2.raw"; 110 $f1 = $key_file; //get_hidden_file("keys2.txt"); 111 $f2 = $value_file; //get_hidden_file("values2.txt"); 108 $key_file = get_hidden_file("keys2.raw"); 109 $value_file = get_hidden_file("values2.raw"); 112 110 $exp_time = time() - DAY; 113 111 if (!file_exists($key_file) || filemtime($key_file) < $exp_time || !file_exists($value_file) || filemtime($value_file) < $exp_time) { … … 121 119 trace("KEY.{$c1} VAL.{$c2}"); 122 120 if ($c1 <= 1 || $c2 <= 1) { 123 update_raw($ f1, $f2)->run();121 update_raw($key_file, $value_file)->run(); 124 122 } 125 123 else { -
bitfire/trunk/startup.php
r3111384 r3334399 87 87 }, ARRAY_FILTER_USE_BOTH); 88 88 89 // user is not logged in, so we will run the firewall code here.90 // if th ey are logged in, the code will run from the wordpress handler so that we91 // have access to the user functions92 if ( empty(CFG::str("cms_root")) || count($auth_cookies) < 2) {89 // if no auth cookies are present or not running in WordPress we can run early 90 // if this code skips - it will be called on plugin init later. 91 // inspect() prevents double runs 92 if (!defined('ABSPATH') || count($auth_cookies) < 2) { 93 93 $bitfire = \Bitfire\BitFire::get_instance(); 94 94 $bitfire->inspect(); -
bitfire/trunk/uninstall.php
r3250641 r3334399 39 39 $file = ini_get("auto_prepend_file"); 40 40 if (!empty($file) && contains($file, "bitfire")) { 41 $seconds = -1;//$exp - time(); 41 $removed = removeBitFireBlock($_SERVER['DOCUMENT_ROOT'] . '/.user.ini'); 42 $seconds = -1; // some servers dont let us read this ini setting, so just display the default 42 43 if ($seconds < 0 || $seconds > 10000) { $seconds = 300; } 43 die(" must wait up to $seconds seconds for user.ini cache to expire, or restart php process.");44 die("Auto startup script has been removed but is still loaded in cache. Please wait up to $seconds seconds for .user.ini cache to expire and auto startup to unload. FAILURE TO REMOVE THE STARTUP SCRIPT FROM .user.ini WILL RESULT IN SERVER CRASH. If you think this is in error - manually edit " . $_SERVER['DOCUMENT_ROOT'] . "/.user.ini in your webroot directory and remove the bitfire startup script manually. email [email protected] if you need assistance."); 44 45 } 45 46 … … 53 54 } 54 55 56 57 /** 58 * manually remove the bitfire startup script. 59 * @param string $filepath 60 * @return bool 61 */ 62 function removeBitFireBlock(string $filepath): bool { 63 if (!file_exists($filepath) || !is_readable($filepath) || !is_writable($filepath)) { 64 return false; 65 } 66 67 $content = file_get_contents($filepath); 68 if ($content === false) { 69 return false; 70 } 71 72 // Remove everything between #BEGIN BitFire and #END BitFire (including those lines) 73 $pattern = '/^#BEGIN BitFire.*?#END BitFire\s*/ms'; 74 $modified = preg_replace($pattern, '', $content); 75 76 if ($modified === null) { 77 return false; // preg_replace failed 78 } 79 80 return file_put_contents($filepath, $modified) !== false; 81 } -
bitfire/trunk/views/traffic.html
r3250587 r3334399 459 459 This free version of BitFire uses OFFLINE data analysis to flag and identify traffic patterns from log data. 460 460 To secure your site and block this traffic in real-time, please upgrade to realtime generative AI blocking. 461 <a href="https://bitfire.co/p urchase" target="_blank" style="text-decoration: underline;">Upgrade to Real-Time blocking</a>461 <a href="https://bitfire.co/pricing" target="_blank" style="text-decoration: underline;">Upgrade to Real-Time blocking</a> 462 462 </td> 463 463 </tr> 464 464 </table> 465 <table style="margin-top:.25rem" class="{{showerror_class}}"> 466 <tr> 467 <td style="font-size:1rem;border:2px solid #F33;padding:.25rem 1rem;"> 468 <h2 style="margin-bottom:.25rem;float:left;margin-right:2rem;">Note:</h2> 469 ERROR: configuration data wp-content/uploads/bitfire_RANDOM has been removed! Software is in-operable. Please re-install. 470 </td> 471 </tr> 472 </table> 473 465 474 466 475 <!-- CARDS --> -
bitfire/trunk/views/wizard.html
r3234339 r3334399 286 286 </div> 287 287 <div class="modal-body"> 288 <p>Auto configuration complete. Click finish to download IP database (1 min).</p>288 <p>Auto configuration complete. Click Finish -> Tour Dashboard to download IP database (wait ~1 min for IP database to download).</p> 289 289 290 290 <div id="spin" class="text-success hidden spinner-border left mt-1 mr-2 " style="margin-left: 0" role="status"></div>
Note: See TracChangeset
for help on using the changeset viewer.