#!/usr/bin/tclsh # # Copyright (C) 2010 Alexandros Stergiakis # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . # #//# # This is the entry point for MikroConf daemon. Various initialization operations are performed here. # # @assume MikroConf is started with a symlink that points to main.tcl in BASE_DIR. # @assume The required Tcl packages can be found in the default auto_path #//# package require Tcl 8.5 package require Thread 2.6.5 package require unix 0.5 package require Tclx 8.4 package require cmdline 1.3.1 ;# Tcllib package require ip 1.1.3 ;# Tcllib package require fileutil 1.13.6 ;# Tcllib package require textutil 0.7.1 ;# Tcllib package require Memchan 2.2.1 package require vfs 1.3 package require vfs::urltype 1.0 package require vfs::ftp 1.0 package require vfs::webdav 0.1 package require vfs::http 0.6 package require vfs::template 1.5.3 ################################ # Initialization ################################ set booting 1 ;# log messages also to the console while booting set auto_noexec always set auto_noload always # Mutex for safe access and modification of environment variables across threads tsv::set conf ENV_MUTEX [thread::mutex create] # Find and move to directory where MikroConf code resides if {[catch { set BASE_DIR [file normalize [file join [file dirname $argv0] [file dirname [file readlink $argv0]]]] }]} { puts stderr "Error: Failed to find the directory where MikroConf code resides.\nRemember that MikroConf should start with a symlink." exit 1 } cd $BASE_DIR tsv::set conf BASE_DIR $BASE_DIR unset BASE_DIR # Note: After the framework is loaded, the working directory can change freely, # because all paths are resolved using BASE_DIR as the root. # Hardcoded configuration parameters. source defines.tcl # The usual special control sequences due to raw input from keyboard are not interpreted. However, # for enhanced protection we ignore the signals anyway. signal ignore SIGINT ;# Ctrl-C signal ignore SIGABRT ;# Ctrl-\ signal ignore SIGTSTP ;# Ctrl-Z signal unblock {SIGHUP QUIT TERM} signal trap {SIGHUP QUIT TERM} { exit 0 "Received signal to terminate" } signal trap SIGUSR1 ::session::close_all_user_sessions # stdout/stderr translation set to crlf, because it is sent to console. fconfigure stdin -buffering line -translation auto fconfigure stdout -buffering line -translation crlf fconfigure stderr -buffering line -translation crlf ################################ # Command line argument parsing ################################ # Print MikroConf banner puts "[tsv::get conf MCINFO]" set options " {c.arg {[tsv::get conf INIT_CONFIG_FILE]} {Alternative initialization configuration file}} {d {Activate debuging code and generate debugging output on console}} " if {[catch { array set params [::cmdline::getoptions argv $options "\[options]"] } errMsg]} { puts stderr $errMsg exit 1 } tsv::set conf INIT_CONFIG_FILE $params(c) tsv::set conf DEBUGGING $params(d) unset params options ################################ # Load the framework ################################ source lib.tcl source helper.tcl source event.tcl source session.tcl source log.tcl source event.tcl source procedures.tcl source module.tcl source sysconf.tcl source email.tcl source auth.tcl # Note: thread.tcl, cli.tcl, pty.tcl and policy.tcl session-specific, and loaded in Session threads. ################################ # Parse & Check validity of init config file ################################ log::Debug "Reading configuration directives from: [tsv::get conf INIT_CONFIG_FILE]" ::helper::parseconf_interp eval [list namespace eval conf {}] ;# Create the conf namespace ::helper::parseconf_interp eval [list set ::conf::BASE_DIR [tsv::get conf BASE_DIR]] ::helper::parseconf conf [tsv::get conf INIT_CONFIG_FILE] foreach var [list ETC_DIR VAR_DIR LOG_DIR TMP_DIR RAM_DIR PROC_DIR SYS_DIR DEV_DIR MOD_DIR HOME_DIR \ CONF_DIR WORK_DIR MODULES_DIR FIFO_DIR] { set val [tsv::get conf $var] if {[catch { ::helper::checkfs -dir $val c } errMsg]} { log::Error -exit 1 "configuration parameter $var: $errMsg" } } foreach var [list STARTUP_CONFIG_FILE SERVER_PORT] { if {! [tsv::exists conf $var]} { log::Error -exit 1 "configuration parameter $var must be set" } } foreach var [list FACTORY_STARTUP_CONFIG_FILE \ OSINFO_FILE DISTRO_CONFIG_FILE] { set val [tsv::get conf $var] if {[catch { ::helper::checkfs -file "$val" {r w c} } errMsg]} { log::Error -exit 1 "configuration parameter $var: $errMsg" } } foreach var [list SHELL_BIN MC_SHELL NO_SHELL] { set val [tsv::get conf $var] if {[catch { ::helper::checkfs -file "$val" x } errMsg]} { log::Error -exit 1 "configuration parameter $var: $errMsg" } } if {! [string is list [tsv::get conf SKIP_MODULES]]} { log::Error -exit 1 "illegal value for SKIP_MODULES configuration parameter" } foreach var [list DEFAULT_COLS DEFAULT_ROWS MAX_LOGINS SESSION_TIMEOUT USERNAME_MAXLEN PASSWORD_MAXLEN] { set val [tsv::get conf $var] if {! [string is integer $val] || ! (0 < $val)} { log::Error -exit 1 "illegal value for $var configuration parameter" } } foreach var [list MAX_SESSIONS LOGTHREAS_DEF] { set val [tsv::get conf $var] if {! [string is integer $val] || ! (0 <= $val)} { log::Error -exit 1 "illegal value for $var configuration parameter" } } foreach var [list PAM_ID PROMPT_DEF] { set val [tsv::get conf $var] if {[lempty $val]} { log::Error -exit 1 "illegal value for $var configuration parameter" } } unset var val ################################ # Check presence of required busybox applets and features ################################ # First check that busybox is compiled with CONFIG_HAVE_DOT_CONFIG # Note: PATH environment variable should be set properly. if {[tsv::get conf BUSYBOX_CHKS]} { if {[lempty [auto_execok bbconfig]]} { log::Error -exit 1 "Cannot find required binary 'bbconfig'. Please verify that busybox is compiled with CONFIG_HAVE_DOT_CONFIG." } set BCONFIG [split [exec bbconfig] "\n"] } foreach feature [list \ CONFIG_ID \ CONFIG_RESET \ CONFIG_CLEAR \ CONFIG_SLEEP \ CONFIG_TRUE \ CONFIG_FALSE \ CONFIG_SED \ CONFIG_MKFIFO \ CONFIG_MORE \ CONFIG_LESS \ CONFIG_RESIZE \ CONFIG_FEATURE_RESIZE_PRINT \ CONFIG_FEATURE_VI_WIN_RESIZE \ CONFIG_HOSTNAME \ CONFIG_TELNET \ CONFIG_FEATURE_SH_IS_ASH \ CONFIG_KLOGD \ CONFIG_SYSLOGD \ CONFIG_FEATURE_IPC_SYSLOG \ CONFIG_FREE \ CONFIG_UPTIME \ CONFIG_AWK \ CONFIG_SYNC \ CONFIG_GUNZIP \ CONFIG_STTY \ CONFIG_KILLALL \ CONFIG_KILL \ CONFIG_WATCHDOG \ CONFIG_INSMOD \ CONFIG_IFCONFIG \ CONFIG_MOUNT \ CONFIG_BB_SYSCTL \ CONFIG_SWAPONOFF \ CONFIG_LOCK \ CONFIG_GREP \ CONFIG_ECHO \ CONFIG_TOUCH \ CONFIG_MKDIR \ CONFIG_MV \ CONFIG_LN \ CONFIG_CHOWN \ CONFIG_CHMOD \ CONFIG_CROND \ CONFIG_CRONTAB \ CONFIG_ENV \ CONFIG_TTYSIZE \ CONFIG_PIDOF \ CONFIG_BBCONFIG \ CONFIG_WHOAMI \ \ CONFIG_FEATURE_SHADOWPASSWDS \ CONFIG_ADDGROUP \ CONFIG_FEATURE_ADDUSER_TO_GROUP \ CONFIG_DELGROUP \ CONFIG_FEATURE_DEL_USER_FROM_GROUP \ CONFIG_FEATURE_CHECK_NAMES \ CONFIG_ADDUSER \ CONFIG_DELUSER \ CONFIG_GETTY \ CONFIG_FEATURE_UTMP \ CONFIG_FEATURE_WTMP \ CONFIG_LOGIN \ CONFIG_LOGIN_SCRIPTS \ CONFIG_FEATURE_NOLOGIN \ CONFIG_FEATURE_SECURETTY \ CONFIG_PASSWD \ CONFIG_CRYPTPW \ CONFIG_CHPASSWD] { if {! [::helper::busybox_has $feature]} { log::Error -exit 1 "Busybox doesn't have support for required feature: $feature" } } unset feature ################################ # Check presence of required kernel features ################################ if {[tsv::get conf KERNEL_CHKS]} { # Check based on the contents of /proc/config.gz if {[catch { exec gunzip -c -d [file join [tsv::get conf PROC_DIR] config.gz] } KCONFIG]} { log::Error -exit 1 "Could not access kernel's config.gz. Please verify that CONFIG_IKCONFIG and CONFIG_IKCONFIG_PROC kernel features are enabled and that PROC_DIR configuration directive is set correctly to point to the device filesystem." } set KCONFIG [split $KCONFIG "\n"] } foreach feature [list \ CONFIG_IKCONFIG \ CONFIG_IKCONFIG_PROC \ CONFIG_UNIX98_PTYS \ CONFIG_INOTIFY \ CONFIG_INOTIFY_USER \ CONFIG_SYSFS \ CONFIG_POSIX_MQUEUE \ CONFIG_PROC_FS \ CONFIG_UNIX \ CONFIG_INET] { if {! [::helper::kernel_has $feature]} { ;# Can return error. log::Error -exit 1 "Kernel doesn't have support for the required feature: $feature" } } unset feature ################################ # Check that user is root ################################ if {[exec id -u] != 0} { log::Error -exit 1 "you need to be root to run MikroConf" } ################################ # Check if a MikroConf daemon is already running ################################ if {[catch { signal trap SIGALRM list alarm 5.0 ;# (@magic-number) ::comm::comm send [tsv::get conf SERVER_PORT] ping log::Error -exit 1 "a MikroConf daemon is already running" }]} { # Timeout waiting. Seems no other deamon is running. OK signal ignore SIGALRM } ################################ # Restore factory settings if configuration was erased ################################ if {! [file exists [tsv::get conf STARTUP_CONFIG_FILE]]} { log::Warning "Main configuration file is missing, restoring factory settings" file copy -force [tsv::get conf FACTORY_STARTUP_CONFIG_FILE] [tsv::get conf STARTUP_CONFIG_FILE] } ################################ # Program termination & Error recovery ################################ # Prepare MikroConf server for shutdown/reload, cleaning up state. proc cleanup {} { # Wait until the event loop becomes empty. namespace eval ::tmp {} after idle set ::tmp::forever 1 set ::tmp::forever 0 vwait ::tmp::forever cd [tsv::get conf BASE_DIR] ::session::close_all_user_sessions ::session::close_all_bg_sessions # Remove any stale FIFOs catch { file delete [glob -nocomplain -directory [tsv::get conf FIFO_DIR] *] } tsv::unset conf } rename exit _exit # True 'exit' function.This is a wrapper. # # @param code exit code, defalts to 0. # @param reason The reason for MikroConf termination. proc exit {{code 0} {reason "Unspecified"}} { log::Notice "MikroConf deamon is terminating" "Exit code: $code" "Reason: $reason" # Generated before MikroConf terminates. # Helpful for cleaning-up stuff. # # @param The error code # @param The reason for the termination. event generate SERVER EXIT [list $code $reason] cleanup # Warning: The exit bellow can sometimes cause a Tcl panic with message printed: # called Tcl_FindHashEntry on deleted table # However it is safe to ignore. # tDOM is responsible for this. _exit $code } # Background error handling procedure for the Master interpreter # # @param msg The error message to display proc bgerror {msg} { # Note: We don't have to rename bgerror just in case another error occurs within bgerror itself. # The reason is that if another Tcl error occurs within the bgerror procedure then Tcl reports # the error itself by writing a message to stderr. # # If several background errors accumulate before bgerror is invoked to process them, bgerror will # be invoked once for each error, in the order they occurred. However, if bgerror returns with a break # exception, then any remaining errors are skipped without calling bgerror. # # Informationally: This is not the case for "unknown". If an unknown command is executed from # within "unknown", the "unknown" command is executed again recursively. And since the same # code is executed to handle the situation, an endless loop can arise. global errorInfo errorCode log::Alert -stack $errorInfo -code $errorCode "Background error: $msg" } # Custom errorproc to handle background errors arrising from asynchronous script executions # between threads. If we don't use one, then errors are printed to stderr. # bgerror is executed as well, after this. # # @param threadId Thread Id. # @param stacktrace Error info. proc errorproc {threadId stacktrace} { log::Error -stack $stacktrace "Background error in thread $threadId: " $stacktrace } thread::errorproc errorproc ################################ # Register Virtual Filesystems ################################ foreach {url proto} [tsv::get conf PROTOCOL_MAPS] { log::Debug "Registering URL type: $url" vfs::urltype::Mount $proto } foreach {url dir} [tsv::get conf FILESYSTEM_MAPS] { log::Debug "Registering URL type: $url" ::vfs::template::mount -catch 0 -volume $dir $url } ################################ # Load MikroConf Modules ################################ log::Info "Loading MikroConf modules" module init # This module is essential to MikroConf and must be loaded first. module require base ;# left uncaught module loadall ;# internally it skips failed ones ################################ # Distribution Specific Extensions ################################ # Load distribution-specific customizations on the framework. # This normally registers procedures and event handlers source [tsv::get conf DISTRO_CONFIG_FILE] ################################ # Create the Event session ################################ # Create the background session that event handlers are executed. # Unline user sessions, execution in this session is not user-driven, but event-driven. log::Debug "Creating Event Session" tsv::set conf EVENT [::session::create_bg_session EVENT] ################################ # Load Start-up Configuration ################################ # Emitted before Router Configuration is loaded on boot. # Can be used from distributions to copy the startup config # from a safe place, to where MikroConf expects to # find them, before it loads them. # # @param The full path to the startup-config file in the file system where it is expected to be found. event generate STARTUP_CONFIG LOAD [tsv::get conf STARTUP_CONFIG_FILE] log::Info "Loading Start-up Configuration" sysconf loadConf ################################ # Listen for user connections ################################ # We need to reset this value, cause it might have some content when the execution flow reaches this point, and this will cause catcherror to think that the first command caused an error global errorInfo set errorInfo "" ::session::listen_for_connections log::Info "Waiting for clients" log::mqsync set booting 0 ;# Stop printing messages to the console. # Emitted when MikroConf finished loading. event generate SERVER BOOT vwait __forever__