]>
Commit | Line | Data |
---|---|---|
1 | <?php | |
2 | if (ini_get('output_buffering') == true) { | |
3 | ini_set('output_buffering', 'off'); | |
4 | } | |
5 | if (ini_get('zlib.output_compression') == true) { | |
6 | ini_set('zlib.output_compression', 'off'); | |
7 | } | |
8 | ||
9 | function check_requirements() | |
10 | { | |
11 | if (version_compare(PHP_VERSION, '8.0.0', '<')) | |
12 | { | |
13 | die("This webserver is using PHP version ".PHP_VERSION." but we require at least PHP 8.0.0.<br>". | |
14 | "If you already installed PHP8 but are still seeing this error, then it means ". | |
15 | "apache/nginx/.. is loading an older PHP version. Eg. on Debian/Ubuntu you need ". | |
16 | "<code>apt-get install libapache2-mod-php8.2</code> (or a similar version) and ". | |
17 | "<code>apt-get remove libapache2-mod-php7.4</code> (or a similar version). ". | |
18 | "You may also need to choose again the PHP module to load in apache via <code>a2enmod php8.2</code>"); | |
19 | } | |
20 | ||
21 | $loaded_extensions = get_loaded_extensions(); | |
22 | $required_extensions = ["mbstring", "sodium"]; | |
23 | $missing_extensions = []; | |
24 | foreach ($required_extensions as $mod) | |
25 | if (!in_array($mod, $loaded_extensions)) | |
26 | $missing_extensions[] = $mod; | |
27 | ||
28 | if (count($missing_extensions) > 0) | |
29 | { | |
30 | $text = "<html>The following PHP module(s) need to be loaded:<br>\n<ul>\n"; | |
31 | $cmd = 'apt-get install'; | |
32 | foreach($missing_extensions as $mod) | |
33 | { | |
34 | $text .= "<li>$mod</li>\n"; | |
35 | $cmd .= " php-$mod"; | |
36 | } | |
37 | $text .= "</ul>\n"; | |
38 | $text .= "You need to install/enable these PHP packages and restart the webserver.<br>". | |
39 | "If you are on Debian/Ubuntu then run <code>$cmd</code> ". | |
40 | "and restart your webserver (eg: <code>systemctl restart apache2</code> for apache)."; | |
41 | die($text); | |
42 | } | |
43 | } | |
44 | ||
45 | check_requirements(); /* very early !! */ | |
46 | ||
47 | define('UPATH', dirname(__DIR__)); | |
48 | ||
49 | function get_config($setting) | |
50 | { | |
51 | GLOBAL $config; | |
52 | ||
53 | $item = $config; | |
54 | foreach(explode("::", $setting) as $x) | |
55 | { | |
56 | if (isset($item[$x])) | |
57 | $item = $item[$x]; | |
58 | else | |
59 | return NULL; | |
60 | } | |
61 | return $item; | |
62 | } | |
63 | ||
64 | function get_current_page_helper($name, $p, &$title) | |
65 | { | |
66 | if (isset($p["script"])) | |
67 | { | |
68 | if (($p["script"] != '') && str_ends_with($_SERVER['SCRIPT_FILENAME'],$p["script"])) | |
69 | { | |
70 | // MATCH | |
71 | if (isset($p["title"])) | |
72 | $title = $p["title"]; | |
73 | else | |
74 | $title = $name; | |
75 | return $p; | |
76 | } | |
77 | return null; | |
78 | } | |
79 | foreach ($p as $k=>$v) | |
80 | { | |
81 | $ret = get_current_page_helper($k, $v, $title); | |
82 | if ($ret !== null) | |
83 | return $ret; | |
84 | } | |
85 | return null; | |
86 | } | |
87 | ||
88 | /** Get current page and title */ | |
89 | function get_current_page(&$title) | |
90 | { | |
91 | GLOBAL $pages; | |
92 | foreach ($pages as $k=>$v) | |
93 | { | |
94 | $ret = get_current_page_helper($k, $v, $title); | |
95 | if ($ret !== null) | |
96 | return $ret; | |
97 | } | |
98 | } | |
99 | ||
100 | function page_requires_no_config() | |
101 | { | |
102 | if (str_ends_with($_SERVER['SCRIPT_FILENAME'],"install.php") || | |
103 | str_ends_with($_SERVER['SCRIPT_FILENAME'],"installation.php")) | |
104 | { | |
105 | return TRUE; | |
106 | } | |
107 | return FALSE; | |
108 | } | |
109 | ||
110 | function page_requires_no_login() | |
111 | { | |
112 | if (str_ends_with($_SERVER['SCRIPT_FILENAME'],"login/index.php") || | |
113 | page_requires_no_config()) | |
114 | { | |
115 | return TRUE; | |
116 | } | |
117 | return FALSE; | |
118 | } | |
119 | ||
120 | function read_config_file() | |
121 | { | |
122 | GLOBAL $config; | |
123 | GLOBAL $config_transition_unreal_server; | |
124 | ||
125 | $config = Array(); | |
126 | if (!file_exists(UPATH."/config/config.php") && file_exists(UPATH."/config.php")) | |
127 | { | |
128 | require_once UPATH . "/config.php"; | |
129 | require_once UPATH . "/config/compat.php"; | |
130 | } | |
131 | if ((@include(UPATH . "/config/config.php")) !== 1) | |
132 | return false; | |
133 | if (isset($config['unrealircd'])) | |
134 | $config_transition_unreal_server = true; | |
135 | /* Upgrade needed? */ | |
136 | $plugins_modified = false; | |
137 | foreach ($config["plugins"] as $k=>$v) | |
138 | { | |
139 | if ($v == "sql_auth") | |
140 | { | |
141 | $config["plugins"][$k] = "sql_db"; | |
142 | $plugins_modified = true; | |
143 | } else | |
144 | if ($v == "file_auth") | |
145 | { | |
146 | $config["plugins"][$k] = "file_db"; | |
147 | $plugins_modified = true; | |
148 | } | |
149 | } | |
150 | if ($plugins_modified) | |
151 | write_config_file(); | |
152 | ||
153 | return true; | |
154 | } | |
155 | ||
156 | function read_config_db() | |
157 | { | |
158 | GLOBAL $config; | |
159 | ||
160 | if (page_requires_no_config()) | |
161 | return; | |
162 | ||
163 | $merge = DbSettings::get(); | |
164 | /* DB settings overwrite config.php keys: */ | |
165 | $config = array_merge($config, $merge); | |
166 | } | |
167 | ||
168 | function config_is_file_item($name) | |
169 | { | |
170 | if (($name == "plugins") || | |
171 | ($name == "mysql") || | |
172 | ($name == "base_url") || | |
173 | ($name == "secrets")) | |
174 | { | |
175 | return true; | |
176 | } | |
177 | return false; | |
178 | } | |
179 | ||
180 | function write_config_file() | |
181 | { | |
182 | GLOBAL $config; | |
183 | ||
184 | $file_settings = []; | |
185 | foreach($config as $k=>$v) | |
186 | { | |
187 | if (config_is_file_item($k)) | |
188 | $file_settings[$k] = $v; | |
189 | } | |
190 | ||
191 | $cfg_filename = UPATH.'/config/config.php'; | |
192 | $tmpfile = UPATH.'/config/config.tmp.'.bin2hex(random_bytes(8)).'.php'; | |
193 | $fd = fopen($tmpfile, "w"); | |
194 | if (!$fd) | |
195 | die("Could not write to temporary config file $tmpfile.<br>We need write permissions on the config/ directory!<br>"); | |
196 | ||
197 | $str = var_export($file_settings, true); | |
198 | if ($str === null) | |
199 | die("Error while running write_config_file() -- weird!"); | |
200 | if (!fwrite($fd, "<?php\n". | |
201 | "/* This config file is written automatically by the UnrealIRCd webpanel.\n". | |
202 | " * You are not really supposed to edit it manually.\n". | |
203 | " */\n". | |
204 | '$config = '.$str.";\n")) | |
205 | { | |
206 | die("Error writing to config file $tmpfile (on fwrite).<br>"); | |
207 | } | |
208 | if (!fclose($fd)) | |
209 | die("Error writing to config file $tmpfile (on close).<br>"); | |
210 | /* Now atomically rename the file */ | |
211 | if (!rename($tmpfile, $cfg_filename)) | |
212 | die("Could not write (rename) to file ".$cfg_filename."<br>"); | |
213 | if (function_exists('opcache_invalidate')) | |
214 | opcache_invalidate($cfg_filename); | |
215 | ||
216 | /* Do not re-read config, as it would reinitialize config | |
217 | * without having the DB settings read. (And it also | |
218 | * serves no purpose) | |
219 | */ | |
220 | return true; | |
221 | } | |
222 | ||
223 | // XXX: handle unsetting of config items :D - explicit unset function ? | |
224 | ||
225 | function write_config($setting = null) | |
226 | { | |
227 | GLOBAL $config; | |
228 | ||
229 | /* Specific request? Easy, write only this setting to the DB (never used for file) */ | |
230 | if ($setting !== null) | |
231 | { | |
232 | return DbSettings::set($setting, $config[$setting]); | |
233 | } | |
234 | ||
235 | /* Otherwise write the whole config. | |
236 | * TODO: avoid writing settings file if unneeded, | |
237 | * as it is more noisy than db settings. | |
238 | */ | |
239 | $db_settings = []; | |
240 | ||
241 | foreach($config as $k=>$v) | |
242 | { | |
243 | if (!config_is_file_item($k)) | |
244 | $db_settings[$k] = $v; | |
245 | } | |
246 | ||
247 | if (!write_config_file()) | |
248 | return false; | |
249 | ||
250 | foreach($db_settings as $k=>$v) | |
251 | { | |
252 | $ret = DbSettings::set($k, $v); | |
253 | if (!$ret) | |
254 | return $ret; | |
255 | } | |
256 | ||
257 | return true; | |
258 | } | |
259 | ||
260 | function get_version() | |
261 | { | |
262 | $fd = @fopen(UPATH."/.git/FETCH_HEAD", "r"); | |
263 | if ($fd === false) | |
264 | return "unknown"; | |
265 | $line = fgets($fd, 512); | |
266 | fclose($fd); | |
267 | $commit = substr($line, 0, 8); | |
268 | return $commit; /* short git commit id */ | |
269 | } | |
270 | ||
271 | function generate_secrets() | |
272 | { | |
273 | GLOBAL $config; | |
274 | ||
275 | if (!isset($config['secrets'])) | |
276 | $config['secrets'] = Array(); | |
277 | ||
278 | if (!isset($config['secrets']['pepper'])) | |
279 | $config['secrets']['pepper'] = rtrim(base64_encode(random_bytes(16)),'='); | |
280 | ||
281 | if (!isset($config['secrets']['key'])) | |
282 | $config['secrets']['key'] = rtrim(base64_encode(sodium_crypto_aead_xchacha20poly1305_ietf_keygen()),'='); | |
283 | } | |
284 | ||
285 | function get_active_rpc_server() | |
286 | { | |
287 | $servers = get_config("unrealircd"); | |
288 | if (empty($servers)) | |
289 | return; | |
290 | // TODO: make user able to override this - either in user or in session | |
291 | ||
292 | foreach ($servers as $displayname=>$e) | |
293 | { | |
294 | if (isset($e["default"]) && $e["default"]) | |
295 | return $displayname; | |
296 | } | |
297 | return null; | |
298 | } | |
299 | ||
300 | /* Set a new default RPC server */ | |
301 | function set_default_rpc_server($name) | |
302 | { | |
303 | GLOBAL $config; | |
304 | ||
305 | /* Mark all other servers as non-default */ | |
306 | foreach ($config["unrealircd"] as $n=>$e) | |
307 | if ($n != $name) | |
308 | $config["unrealircd"][$n]["default"] = false; | |
309 | $config["unrealircd"][$name]["default"] = true; | |
310 | } | |
311 | ||
312 | /* Ensure at least 1 server is default */ | |
313 | function set_at_least_one_default_rpc_server() | |
314 | { | |
315 | GLOBAL $config; | |
316 | ||
317 | $has_default_rpc_server = false; | |
318 | foreach ($config["unrealircd"] as $name=>$e) | |
319 | if ($e["default"]) | |
320 | $has_default_rpc_server = true; | |
321 | if (!$has_default_rpc_server) | |
322 | { | |
323 | /* Make first server in the list the default */ | |
324 | foreach ($config["unrealircd"] as $name=>$e) | |
325 | { | |
326 | $config["unrealircd"][$name]["default"] = true; | |
327 | break; | |
328 | } | |
329 | } | |
330 | } | |
331 | ||
332 | function secret_encrypt(string $text) | |
333 | { | |
334 | GLOBAL $config; | |
335 | ||
336 | $key = base64_decode($config['secrets']['key']); | |
337 | $nonce = \random_bytes(\SODIUM_CRYPTO_AEAD_XCHACHA20POLY1305_IETF_NPUBBYTES); | |
338 | $encrypted_text = sodium_crypto_aead_xchacha20poly1305_ietf_encrypt($text, '', $nonce, $key); | |
339 | return "secret:".rtrim(base64_encode($nonce),'=').':'.rtrim(base64_encode($encrypted_text),'='); // secret:base64(NONCE):base64(ENCRYPTEDTEXT) | |
340 | } | |
341 | ||
342 | function secret_decrypt(string $crypted) | |
343 | { | |
344 | GLOBAL $config; | |
345 | ||
346 | $key = base64_decode($config['secrets']['key']); | |
347 | $d = explode(":", $crypted); | |
348 | if (count($d) != 3) | |
349 | return null; | |
350 | $nonce = base64_decode($d[1]); | |
351 | $ciphertext = base64_decode($d[2]); | |
352 | ||
353 | $ret = sodium_crypto_aead_xchacha20poly1305_ietf_decrypt($ciphertext, '', $nonce, $key); | |
354 | if ($ret === false) | |
355 | return null; | |
356 | return $ret; | |
357 | } | |
358 | ||
359 | function upgrade_check() | |
360 | { | |
361 | GLOBAL $config_transition_unreal_server; | |
362 | GLOBAL $config; | |
363 | ||
364 | /* Moving of a config.php item to DB: */ | |
365 | if ($config_transition_unreal_server) | |
366 | write_config(); | |
367 | ||
368 | /* Our own stuff may need upgrading.. */ | |
369 | /* - generating secrets */ | |
370 | if (!isset($config['secrets'])) | |
371 | { | |
372 | generate_secrets(); | |
373 | write_config_file(); | |
374 | } | |
375 | /* - encrypting rpc_password */ | |
376 | if (isset($config['unrealircd']) && | |
377 | isset($config['unrealircd']['rpc_password']) && | |
378 | !str_starts_with($config['unrealircd']['rpc_password'], "secret:")) | |
379 | { | |
380 | $ret = secret_encrypt($config['unrealircd']['rpc_password']); | |
381 | if ($ret !== false) | |
382 | { | |
383 | $config['unrealircd']['rpc_password'] = $ret; | |
384 | write_config('unrealircd'); | |
385 | } | |
386 | } | |
387 | /* $config["unrealircd"] should be an array now.. */ | |
388 | if (isset($config['unrealircd']) && isset($config['unrealircd']['rpc_password'])) | |
389 | { | |
390 | $config["unrealircd"]["default"] = true; | |
391 | $config['unrealircd'] = [ | |
392 | "Primary" => $config['unrealircd']]; | |
393 | write_config("unrealircd"); | |
394 | } | |
395 | ||
396 | $version = get_version(); | |
397 | if (!isset($config['webpanel_version'])) | |
398 | $config['webpanel_version'] = ''; | |
399 | if ($version != $config['webpanel_version']) | |
400 | { | |
401 | $versioninfo = [ | |
402 | "old_version" => $config['webpanel_version'], | |
403 | "new_version" => $version | |
404 | ]; | |
405 | /* And inform the hook (eg the database backends) */ | |
406 | Hook::run(HOOKTYPE_UPGRADE, $versioninfo); | |
407 | /* And set the new version now that the upgrade is "done" */ | |
408 | $config['webpanel_version'] = $version; | |
409 | write_config("webpanel_version"); | |
410 | } | |
411 | } | |
412 | ||
413 | function panel_start_session($user = false) | |
414 | { | |
415 | if (!isset($_SESSION)) | |
416 | { | |
417 | session_set_cookie_params(86400); // can't set this to session_timeout due to catch-22 | |
418 | session_start(); | |
419 | } | |
420 | ||
421 | if ($user === false) | |
422 | { | |
423 | $user = unreal_get_current_user(); | |
424 | if ($user === false) | |
425 | return false; | |
426 | } | |
427 | ||
428 | $timeout = 3600; | |
429 | if (isset($user->user_meta['session_timeout'])) | |
430 | $timeout = (INT)$user->user_meta['session_timeout']; | |
431 | ||
432 | if (!isset($_SESSION['session_timeout'])) | |
433 | $_SESSION['session_timeout'] = $timeout; | |
434 | ||
435 | $_SESSION['last-activity'] = time(); | |
436 | return true; | |
437 | } | |
438 | ||
439 | /* Now read the config, and redirect to install screen if we don't have it */ | |
440 | $config_transition_unreal_server = false; | |
441 | if (!read_config_file()) | |
442 | { | |
443 | if (page_requires_no_config()) | |
444 | { | |
445 | /* Allow empty conf */ | |
446 | } else | |
447 | if (!file_exists(UPATH."/config/config.php") && !file_exists(UPATH."/config.php")) | |
448 | { | |
449 | header("Location: settings/install.php"); | |
450 | die(); | |
451 | } | |
452 | } | |
453 | ||
454 | require_once UPATH . "/Classes/class-hook.php"; | |
455 | if (!is_dir(UPATH . "/vendor")) | |
456 | die("The vendor/ directory is missing. Most likely the admin forgot to run 'composer install'\n"); | |
457 | require_once UPATH . '/vendor/autoload.php'; | |
458 | require_once UPATH . "/Classes/class-cmodes.php"; | |
459 | require_once UPATH . "/inc/defines.php"; | |
460 | require_once UPATH . "/misc/strings.php"; | |
461 | require_once UPATH . "/misc/channel-lookup-misc.php"; | |
462 | require_once UPATH . "/misc/user-lookup-misc.php"; | |
463 | require_once UPATH . "/misc/server-lookup-misc.php"; | |
464 | require_once UPATH . "/misc/ip-whois-misc.php"; | |
465 | require_once UPATH . "/Classes/class-log.php"; | |
466 | require_once UPATH . "/Classes/class-message.php"; | |
467 | require_once UPATH . "/Classes/class-rpc.php"; | |
468 | require_once UPATH . "/Classes/class-paneluser.php"; | |
469 | require_once UPATH . "/Classes/class-notes.php"; | |
470 | require_once UPATH . "/Classes/class-plugins.php"; | |
471 | ||
472 | /* Do various checks and reading, except during setup step 1. */ | |
473 | if (!page_requires_no_config()) | |
474 | { | |
475 | /* Now that plugins are loaded, read config from DB */ | |
476 | read_config_db(); | |
477 | ||
478 | /* Check if anything needs upgrading (eg on panel version change) */ | |
479 | upgrade_check(); | |
480 | ||
481 | /* And a check... */ | |
482 | if (!get_config("base_url")) | |
483 | die("The base_url was not found in your config. Setup went wrong?"); | |
484 | } | |
485 | ||
486 | $pages = [ | |
487 | "Overview" => ["script"=>""], | |
488 | "Users" => ["script"=>"users/index.php"], | |
489 | "Channels" => ["script"=>"channels/index.php"], | |
490 | "Servers" => ["script"=>"servers/index.php"], | |
491 | "Server Bans" => [ | |
492 | "Server Bans" => ["script" => "server-bans/index.php"], | |
493 | "Name Bans" => ["script" => "server-bans/name-bans.php"], | |
494 | "Ban Exceptions" => ["script" => "server-bans/ban-exceptions.php"], | |
495 | ], | |
496 | "Spamfilter" => ["script" => "server-bans/spamfilter.php"], | |
497 | "Logs" => ["script" => "logs/index.php"], | |
498 | "Tools" => [ | |
499 | "IP WHOIS" => ["script" => "tools/ip-whois.php","no_irc_server_required"=>true], | |
500 | ], | |
501 | "Settings" => [ | |
502 | "General Settings" => ["script" => "settings/general.php"], | |
503 | "RPC Servers" => ["script" => "settings/rpc-servers.php","no_irc_server_required"=>true], | |
504 | ], | |
505 | ]; | |
506 | ||
507 | ||
508 | if (!panel_start_session()) | |
509 | { | |
510 | if (!page_requires_no_login()) | |
511 | { | |
512 | if (!is_auth_provided()) | |
513 | die("No authentication plugin loaded. You must load either sql_db, file_db, or a similar auth plugin!"); | |
514 | $current_page = $_SERVER['REQUEST_URI']; | |
515 | header("Location: ".get_config("base_url")."login/?redirect=".urlencode($current_page)); | |
516 | die; | |
517 | } | |
518 | } else { | |
519 | $pages["Settings"]["Accounts"] = [ | |
520 | "script" => "settings/index.php", | |
521 | "no_irc_server_required"=>true | |
522 | ]; | |
523 | if (current_user_can(PERMISSION_MANAGE_USERS)) | |
524 | { | |
525 | $pages["Settings"]["Role Editor"] = [ | |
526 | "script"=>"settings/user-role-edit.php", | |
527 | "no_irc_server_required"=>true | |
528 | ]; | |
529 | } | |
530 | if (current_user_can(PERMISSION_MANAGE_PLUGINS)) | |
531 | { | |
532 | $pages["Settings"]["Plugins"] = ["script" => "settings/plugins.php"]; | |
533 | } | |
534 | $user = unreal_get_current_user(); | |
535 | if ($user) | |
536 | { | |
537 | /* Add logout page, if logged in */ | |
538 | $pages["Logout"] = [ | |
539 | "script"=>"login/?logout=true", | |
540 | "no_irc_server_required"=>true | |
541 | ]; | |
542 | } | |
543 | } | |
544 | ||
545 | Hook::run(HOOKTYPE_NAVBAR, $pages); | |
546 | ||
547 | /* Example to add new menu item: | |
548 | * | |
549 | * class MyPlugin | |
550 | * { | |
551 | * | |
552 | * function __construct() | |
553 | * { | |
554 | * Hook::func(HOOKTYPE_NAVBAR, [$this, 'add_menu']) | |
555 | * } | |
556 | * | |
557 | * function add_menu(&$pages) // this should pass by reference (using the & prefix) | |
558 | * { | |
559 | * $page_name = "My New Page"; | |
560 | * $page_link = "link/to/page.php"; | |
561 | * $pages[$page_name] = $page_link; | |
562 | * } | |
563 | * } | |
564 | */ | |
565 | ||
566 | $current_page = get_current_page($current_page_name); | |
567 | ||
568 | ||
569 | global $rightClickMenu; | |
570 | $rightClickMenu = [ | |
571 | [ | |
572 | "text" => "Copy", | |
573 | "onclick" => "copy_to_clipboard(window.getSelection().toString())", | |
574 | "icon" => "fa-clipboard" | |
575 | ], | |
576 | [ | |
577 | "text" => "Paste", | |
578 | "onclick" => "paste_from_clipboard()", | |
579 | "icon" => "fa-paint-brush", | |
580 | ], | |
581 | ]; | |
582 | ||
583 | // register our menu | |
584 | Hook::run(HOOKTYPE_RIGHTCLICK_MENU, $rightClickMenu); |