# # 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 . # #//# # The "base" module provides generic commands, such as "exit" and "reload", as well as commands pertaining terminal/console configuration and authentication. #//# module provide base 1.0 namespace eval ::module::base { namespace import ::helper::* ::module::api::* namespace export dlist_procedures dlist_avail_procs dlist_all_procs auth_username_callback proc check {} { foreach feature [list \ CONFIG_ASH \ CONFIG_FEATURE_SH_IS_ASH \ CONFIG_UPTIME \ CONFIG_KILL \ CONFIG_VI \ \ CONFIG_LOGIN \ CONFIG_INIT \ CONFIG_FEATURE_USE_INITTAB \ CONFIG_FEATURE_KILL_REMOVED] { if {! [::helper::busybox_has $feature]} { ;# Can return error. error "Busybox doesn't have support for $feature." } } # No busybox config variable for 'reboot' applet', so we check presense in the filesystem. if {[lempty [auto_execok reboot]]} { error "Could not find busybox applet \"reboot\"" } } proc description {} { return "Generic MikroConf services" } proc version {} { # Version info for 'base' is printed in 'ShVersion' command. } proc constructor {} { log::Info "Loading \"base\" module: [description]" Global ETC_DIR variable INIT_FILE [file join $ETC_DIR inittab] check # Store the original values, because they might change later. Global HISTORY_DEF FLOW_DEF SYNCH_DEF PROMPT_DEF ANSI_DEF variable history_def $HISTORY_DEF variable flow_def $FLOW_DEF variable synch_def $SYNCH_DEF variable prompt_def $PROMPT_DEF variable ansi_def $ANSI_DEF reset # Finally load Command Specs sysconf loadspecs "modules/base/base.specs" } proc destructor {} { # First unload Command Specs sysconf remove "base" reset } proc reset {} { Global PRIV ADMIN ADMIN_PASS USER CONSOLE_USER ETC_DIR SHELL_BIN NO_SHELL MC_SHELL CON_SHELL file delete [glob -nocomplain -type f -- [file join $ETC_DIR passwd]+] file delete [glob -nocomplain -type f -- [file join $ETC_DIR shadow]+] file delete [glob -nocomplain -type f -- [file join $ETC_DIR group]+] fileutil::writeFile [file join $ETC_DIR passwd] "" fileutil::writeFile [file join $ETC_DIR shadow] "" fileutil::writeFile [file join $ETC_DIR group] "" auth adduser root $NO_SHELL auth makeroot root if {! [lempty $ADMIN] && ! [lempty $ADMIN_PASS] } { auth adduser $ADMIN $SHELL_BIN $ADMIN_PASS auth makeroot $ADMIN } auth adduser $PRIV $NO_SHELL auth adduser $USER $NO_SHELL auth adduser $CONSOLE_USER $CON_SHELL auth makeroot $CONSOLE_USER # Mapping from user name to password (possibly encrypted). variable user_accounts array set user_accounts {} # Set to the encrypted password string. Empty if not enable password is set. variable enable_pass {} variable console_login 0 variable console_pass {} inittab_reset # Restore default terminal settings Global -rw HISTORY_DEF FLOW_DEF SYNCH_DEF PROMPT_DEF ANSI_DEF variable history_def variable flow_def variable synch_def variable prompt_def variable ansi_def set HISTORY_DEF $history_def set FLOW_DEF $flow_def set SYNCH_DEF $synch_def set PROMPT_DEF $prompt_def set ANSI_DEF $ansi_def # Reset all banners to empty string. Global -rw USER_BANNER PRIV_BANNER set USER_BANNER "" set PRIV_BANNER "" variable banner_motd "" variable banner_login "" } ################ # Exported Procedures ################ # Register callback procs to be called when renaming an interface. proc auth_username_callback {name} { variable auth_username_callbacks lappend auth_username_callbacks $name } ################ # Command Handlers ################ command Exit {cmdline argstart sid out no arguments args} { sysconf confmode $sid exit return } command End {cmdline argstart sid out no arguments args} { sysconf confmode $sid init return } command Terminate {cmdline argstart sid out no arguments args} { Session $sid USERNAME exit 0 "User \"$USERNAME\" requested termination from session $sid" } command ConfTerminal {cmdline argstart sid out no arguments args} { sysconf confmode $sid set "global" return } # @todo Print uclibc version command ShVersion {cmdline argstart sid out no arguments args} { global tcl_platform Global OSINFO_FILE MCINFO_1LINER VERSION PROC_DIR set osinfo [::fileutil::cat $OSINFO_FILE] set kernel [::fileutil::cat [file join $PROC_DIR "version"]] ;# Note: More info than 'uname' set busybox [lindex [split [exec busybox --help] "\n"] 0] ;# Get the first line of output set uptime [exec uptime] set bootpar [::fileutil::cat [file join $PROC_DIR "cmdline"]] set platform [array get tcl_platform] set modules [::module::loaded] set memory [dict remove [sysinfo] "uptime" "loads"] set tcl "[info patchlevel] [info library]" puts $osinfo puts "MikroConf: $MCINFO_1LINER" puts "Loaded modules: $modules" puts "Busybox: $busybox" puts "Tcl: $tcl" puts "Platform: $platform" puts "Memory: $memory" puts -nonewline $out "Kernel: $kernel" puts -nonewline $out "Boot Param: $bootpar" puts "Uptime: $uptime" puts "Module info:" foreach mod $modules { puts "Module ${mod}-[::module::version $mod]: [::module::${mod}::description]" set v [::module::${mod}::version] if {! [lempty $v]} { puts $v } } return } command ShMikroConfModules {cmdline argstart sid out no arguments args} { set loaded [module loaded] puts "Available MikroConf Modules. Loaded marked with '*'" foreach {file name ver} [module available] { if {$name in $loaded} { puts -nonewline { * } } else { puts -nonewline { } } puts "$name-$ver" } } # @todo Save reason somewhere so that we can display after reboot as the reason for last reboot. command Reload {cmdline argstart sid out no arguments args} { Session $sid USERNAME if {[dict exists $arguments MESSAGE]} { set msg [dict get $arguments MESSAGE] } else { set msg "User request" } if {[dict exists $arguments modules]} { module reloadall } elseif {[dict exists $arguments warm]} { # We rely in init to re-run MikroConf. exit 0 "User \"$USERNAME\" requested termination from session $sid" } else { exec reboot exit 0 "User \"$USERNAME\" requested a cold reboot from session $sid; reason: $msg" } return } ################################ # Authentication ################################ # @todo Provide busybox feedback for password strength. # @assume A password prefix of '$1$' indicates MD5 encryption of provided password. # @bug Start asking for usernames after this command is issued. command Username {cmdline argstart sid out no arguments args} { Global ADMIN USER PRIV CONSOLE_USER VTY_USER MC_SHELL variable auth_username_callbacks variable user_accounts set user [dict get $arguments USERNAME] if {! [auth name_is_ok $user]} { ferror "invalid username string" [getpos USERNAME] } if {$user in "root $ADMIN $USER $CONSOLE_USER $VTY_USER $PRIV"} { ferror "username is reserved" [getpos USERNAME] } if {$no} { if {$user ni [array names user_accounts]} { ferror "unknown username" [getpos USERNAME] } auth deluser $user unset user_accounts($user) # When the last Username is removed, then switch to Password-only login. if {[lempty [array names user_accounts]]} { foreach cb $auth_username_callbacks { $cb false } auth unlock $CONSOLE_USER inittab_reset } return } set pass [dict get $arguments PASSWORD] # Cases for PASSWORD: # 1. It is provided on the command line as cleartext, that should be encrypted before stored in config. # 2. The command is executed on system boot and PASSWORD is provided already encrypted. # Note: A user could also provide a pre-computed MD5 checksum for enable password # on the command line. In which case the supplied password must begin with '$1$'. if {[string match {$1$*} $pass]} { # Case 2. auth adduser $user $MC_SHELL ;# error propagates auth passwd $user $pass -nocrypt ;# error propagates } else { # Case 1. if {! [auth pass_is_ok $pass]} { ferror "invalid password string" [getpos PASSWORD] } set pass [auth adduser $user $MC_SHELL $pass] ;# error propagates } set user_accounts($user) $pass # With the first entry, switch to Username/Password login. if {[llength [array names user_accounts]] == 1} { foreach cb $auth_username_callbacks { $cb true } auth lock $CONSOLE_USER inittab_reset } return } proc print_Username {} { variable user_accounts set result {} foreach user [array names user_accounts] { append result "username $user password {$user_accounts($user)}\n" } if {! [lempty $result]} { set result "#\n# Local User Accounts:\n${result}\n" } return $result } # @interactive command Enable {cmdline argstart sid out no arguments args} { Global PRIV PRIV_BANNER variable enable_pass if {[sysconf authmode $sid get] eq "priv"} { error "already in priviledged mode" } if {[lempty $enable_pass] || [auth login $PRIV]} { sysconf authmode $sid set "priv" log::Info "User raised to priviledged level in session $sid" } else { error "authentication failed" } # Print Privilege Banner # We cannot use 'puts' or 'return' because the interpreter it would print is dead now. thread::send -async $sid [list puts -nonewline [? [lempty $PRIV_BANNER] "" "${PRIV_BANNER}\n"]] return } command Disable {cmdline argstart sid out no arguments args} { sysconf authmode $sid set "user" return } # @todo Provide busybox feedback for password strength. # @assume A password prefix of '$1$' indicates MD5 encryption of password. command EnablePassword {cmdline argstart sid out no arguments args} { Global PRIV variable enable_pass if {$no} { set enable_pass {} auth lock $PRIV return } set pass [dict get $arguments PASSWORD] if {[string match {$1$*} $pass]} { auth passwd $PRIV $pass -nocrypt ;# error propagates set enable_pass $pass } else { if {! [auth pass_is_ok $pass]} { ferror "invalid password string" [getpos PASSWORD] } set enable_pass [auth passwd $PRIV $pass] ;# error propagates } return } proc print_EnablePassword {} { variable enable_pass if {! [lempty $enable_pass]} { return "enable password {$enable_pass}\n" } return } ################################ # System Configuration ################################ command ShXml {cmdline argstart sid out no arguments args} { return [sysconf print] } command ShRunning {cmdline argstart sid out no arguments args} { return [sysconf runningConf] } command ShStartup {cmdline argstart sid out no arguments args} { return [sysconf startupConf] } command Write {cmdline argstart sid out no arguments args} { sysconf saveConf return } # @todo Security considerations: Implement virtual chroot within *_DIR for each url type. command Copy {cmdline argstart sid out no arguments args} { lassign $arguments src sval dst dval if {$src eq "startup-config" && $dst eq "running-config"} { module resetall sysconf loadConf return } if {$src eq "running-config" && $dst eq "startup-config"} { sysconf saveConf return } switch -exact -- $src { "startup-config" { Global STARTUP_CONFIG_FILE set sval $STARTUP_CONFIG_FILE } "running-config" { set sval [::fileutil::tempfile] fileutil::writeFile $sval [runningConf] } "URL" { # Leave sval as it is. } } switch -exact -- $dst { "startup-config" { Global STARTUP_CONFIG_FILE file copy -force -- $sval $STARTUP_CONFIG_FILE } "running-config" { module resetall sysconf loadConf [::fileutil::cat $sval] catch { file delete -force -- $sval } } "URL" { file copy -force -- $sval $dval } } return } command EraseStartup {cmdline argstart sid out no arguments args} { Global STARTUP_CONFIG_FILE file delete -force -- $STARTUP_CONFIG_FILE return } command EraseRunning {cmdline argstart sid out no arguments args} { module resetall foreach p [procedures getlist] { procedures unregister $p } return } ################################ # Terminal ################################ # @limit The setting is discarted when window size changes as reported by shell. # @diff Use of this instead of "terminal length" & "terminal width". command TermWindow {cmdline argstart sid out no arguments args} { Session -rw $sid ROWS COLUMNS if {$no} { Global DEFAULT_ROWS DEFAULT_COLS set ROWS $DEFAULT_ROWS set COLUMNS $DEFAULT_COLS } else { set ROWS [dict get $arguments ROWS] set COLUMNS [dict get $arguments COLUMNS] } seval [list ::cli::handleAction RedrawScreen] return } command TermAnsi {cmdline argstart sid out no arguments args} { Session -rw $sid ANSI if {$no} { set ANSI 0 } else { set ANSI 1 } return } command TermAnsi_config {cmdline argstart sid out no arguments args} { Global -rw ANSI_DEF if {$no} { set ANSI_DEF 0 } else { set ANSI_DEF 1 } return } proc print_TermAnsi_config {} { Global ANSI_DEF variable ansi_def if {$ansi_def ne $ANSI_DEF} { if {$ANSI_DEF} { return "terminal advanced-ansi\n" } else { return "no terminal advanced-ansi\n" } } return } command TermPrompt {cmdline argstart sid out no arguments args} { Session -rw $sid PROMPT if {$no} { Global PROMPT_DEF set PROMPT $PROMPT_DEF } else { set PROMPT [dict get $arguments PROMPT] } return } command TermPrompt_config {cmdline argstart sid out no arguments args} { Global -rw PROMPT_DEF if {$no} { variable prompt_def set PROMPT_DEF $prompt_def } else { set PROMPT_DEF [dict get $arguments PROMPT] } return } proc print_TermPrompt_config {} { Global PROMPT_DEF variable prompt_def if {$prompt_def ne $PROMPT_DEF} { return "terminal prompt $PROMPT_DEF\n" } return } command TermHistory {cmdline argstart sid out no arguments args} { Session -rw $sid HISTORY HISTORY_SIZE HISTORY_ENTRIES set size [dict get $arguments SIZE] if {! [string is integer $size] || $size < 0 || $size > 256} { ;# (@magic-number) ferror "must be a number in the range range 0..256" [getpos SIZE] } set HISTORY_SIZE $size if {$HISTORY_ENTRIES > $size} { set HISTORY_ENTRIES $size incr size -1 ;# because "lrange {1 2 3} end-0 end" gives "3". set HISTORY [lrange $HISTORY end-$size end] } return } command TermHistory_config {cmdline argstart sid out no arguments args} { Global -rw HISTORY_DEF set size [dict get $arguments SIZE] if {! [string is integer $size] || $size < 0 || $size > 256} { ;# (@magic-number) ferror "must be a number in the range range 0..256" [getpos SIZE] } set HISTORY_DEF $size return } proc print_TermHistory_config {} { Global HISTORY_DEF variable history_def if {$history_def ne $HISTORY_DEF} { return "terminal history $HISTORY_DEF\n" } return } # @diff Use of this instead of "terminal length 0" command TermFlow {cmdline argstart sid out no arguments args} { Session -rw $sid FLOW if {$no} { set FLOW 0 } else { set FLOW 1 } return } # @diff Use of this instead of "terminal length 0" command TermFlow_config {cmdline argstart sid out no arguments args} { Global -rw FLOW_DEF if {$no} { set FLOW_DEF 0 } else { set FLOW_DEF 1 } return } proc print_TermFlow_config {} { Global FLOW_DEF variable flow_def if {$flow_def ne $FLOW_DEF} { if {$FLOW_DEF} { return "terminal flow-control\n" } else { return "no terminal flow-control\n" } } return } command TermSynch {cmdline argstart sid out no arguments args} { Session -rw $sid SYNCH if {$no} { set SYNCH 0 } else { set SYNCH 1 } return } command TermSynch_config {cmdline argstart sid out no arguments args} { Global -rw SYNCH_DEF if {$no} { set SYNCH_DEF 0 } else { set SYNCH_DEF 1 } return } proc print_TermSynch_config {} { Global SYNCH_DEF variable synch_def if {$synch_def ne $SYNCH_DEF} { if {$SYNCH_DEF} { return "terminal synchronous\n" } else { return "no terminal synchronous\n" } } return } command ShHistory {cmdline argstart sid out no arguments args} { Session $sid HISTORY return [join [lreverse $HISTORY] "\n"] } ################################ # Procedures ################################ command RegProc {cmdline argstart sid out no arguments args} { set name [dict get $arguments NAME] if {$no} { procedures unregister $name } else { procedures register $sid $name } return } proc dlist_procedures {sid args} { procedures getlist } proc dlist_avail_procs {sid args} { Session $sid SLAVE seval [list $SLAVE invokehidden info procs] } proc dlist_all_procs {sid args} { ::struct::set union [dlist_procedures $sid] [dlist_avail_procs $sid] } command ShProcedures {cmdline argstart sid out no arguments args} { if {[dict exists $arguments NAME]} { set name [dict get $arguments NAME] lassign [procedures getproc $name] arg body ;# checks it exists. puts "proc $name {$args} {$body}" } else { foreach name [procedures getlist] { lassign [procedures getproc $name] args body puts "proc $name {$args} {$body}\n" } } return } proc print_RegProc {} { set result {} foreach name [procedures getlist] { lassign [procedures getproc $name] args body append result "proc $name {$args} {$body}\n" append result "register procedure $name\n\n" } if {! [lempty $result]} { set result "#\n# List of user defined procedures:\n${result}\n" } return $result } ################################ # Scripting ################################ command Scripting {cmdline argstart sid out no arguments args} { if {$no} { # We don't allow to deactivate scripting on an Events Session. # After all that is what it is all about, to evaluate scripts (event handlers). Global EVENT if {$sid eq $EVENT} { error "scripting cannot be deactivated in this session" } seval [list ::policy::scripting off] } else { seval [list ::policy::scripting on] } return } ################################ # Console Configuration ################################ proc inittab_reset {} { variable console_login variable user_accounts variable INIT_FILE if {$console_login} { if {! [lempty [array names user_accounts]]} { fileutil::writeFile $INIT_FILE \ {::sysinit:/etc/init.d/mikroconf start ::shutdown:/sbin/halt tts/0::askfirst:/bin/login ttyS0::askfirst:/bin/login tty1::askfirst:/bin/login } } else { fileutil::writeFile $INIT_FILE \ {::sysinit:/etc/init.d/mikroconf start ::shutdown:/sbin/halt tts/0::askfirst:/bin/login console ttyS0::askfirst:/bin/login console tty1::askfirst:/bin/login console } } } else { Global CON_SHELL fileutil::writeFile $INIT_FILE \ "::sysinit:/etc/init.d/mikroconf start ::shutdown:/sbin/halt tts/0::askfirst:$CON_SHELL ttyS0::askfirst:$CON_SHELL tty1::askfirst:$CON_SHELL " } kill SIGHUP 1 ;# init } command LineConsole {cmdline argstart sid out no arguments args} { set num [dict get $arguments "0"] if {$num != 0} { ferror "invalid conlose port number" [getpos "0"] } unset num ;# We don't use it sysconf confmode $sid set "console" return } command ConsoleLogin {cmdline argstart sid out no arguments args} { variable console_login if {$no} { set console_login 0 } else { set console_login 1 } inittab_reset return } # @bug sending SIGHUP to init in order to reread inittab will cause current MikroConf session to terminate and ask user to re-login. command ConsolePassword {cmdline argstart sid out no arguments args} { Global CONSOLE_USER variable console_pass variable user_accounts if {$no} { set console_pass {} auth lock $CONSOLE_USER return } set pass [dict get $arguments PASSWORD] if {[string match {$1$*} $pass]} { auth passwd $CONSOLE_USER $pass -nocrypt ;# error propagates } else { if {! [auth pass_is_ok $pass]} { ferror "invalid password string" [getpos PASSWORD] } set pass [auth passwd $CONSOLE_USER $pass] # Be careful not to unlock CONSOLE_USER if Usernames are used. if {[lempty [array list user_accounts]]} { auth unlock $CONSOLE_USER } else { auth lock $CONSOLE_USER } } set console_pass $pass return } proc print_LineConsole {} { variable console_login variable console_pass set result {} if {$console_login || ! [lempty $console_pass]} { append result "#\n# Console Settings:\n" append result "line console 0\n" if {$console_login} { append result "\tlogin\n" } if {! [lempty $console_pass]} { append result "\tpassword {$console_pass}\n" } append result " exit\n\n" } return $result } ################################ # System Banners ################################ command BannerMotd {cmdline argstart sid out no arguments args} { Global ETC_DIR variable banner_login variable banner_motd if {$no} { set banner_motd "" } else { set banner_motd [dict get $arguments TEXT] } fileutil::writeFile [file join $ETC_DIR issue] "[? [lempty $banner_motd] "" ${banner_motd}\n][? [lempty $banner_login] "" ${banner_login}\n]" return } proc print_BannerMotd {} { variable banner_motd if {! [lempty $banner_motd]} { return "banner motd {$banner_motd}\n" } return } command BannerLogin {cmdline argstart sid out no arguments args} { Global ETC_DIR variable banner_motd variable banner_login if {$no} { set banner_login "" } else { set banner_login [dict get $arguments TEXT] } fileutil::writeFile [file join $ETC_DIR issue] "[? [lempty $banner_motd] "" ${banner_motd}\n][? [lempty $banner_login] "" ${banner_login}\n]" return } proc print_BannerLogin {} { variable banner_login if {! [lempty $banner_login]} { return "banner login {$banner_login}\n" } return } command BannerUser {cmdline argstart sid out no arguments args} { Global -rw USER_BANNER if {$no} { set USER_BANNER "" } else { set USER_BANNER [dict get $arguments TEXT] } return } proc print_BannerUser {} { Global USER_BANNER if {! [lempty $USER_BANNER]} { return "banner user {$USER_BANNER}\n" } return } command BannerPrivilege {cmdline argstart sid out no arguments args} { Global -rw PRIV_BANNER if {$no} { set PRIV_BANNER "" } else { set PRIV_BANNER [dict get $arguments TEXT] } return } proc print_BannerPrivilege {} { Global PRIV_BANNER if {! [lempty $PRIV_BANNER]} { return "banner privilege {$PRIV_BANNER}\n" } return } ################################ # Generic Utilities ################################ # @interactive command Escape {cmdline argstart sid out no arguments args} { Global BASE_DIR # We need to execute our own script in order to set the right # terminal settings (stty), change current path, etc.. ptyexec -root -- sh [file join $BASE_DIR escape.sh] return } # @interactive command EditStartup {cmdline argstart sid out no arguments args} { set tempfile [::fileutil::tempfile] # tempfiles will be deleted automatically by other means. Global STARTUP_CONFIG_FILE file copy -force -- $STARTUP_CONFIG_FILE $tempfile ptyexec vi $tempfile file copy -force -- $tempfile $STARTUP_CONFIG_FILE return } # @interactive command EditRunning {cmdline argstart sid out no arguments args} { set tempfile [::fileutil::tempfile] # tempfiles will be deleted automatically by other means. fileutil::writeFile $tempfile [sysconf runningConf] ptyexec vi $tempfile module resetall sysconf loadConf [::fileutil::cat $tempfile] return } # @interactive command EditProc {cmdline argstart sid out no arguments args} { set tempfile [::fileutil::tempfile] # tempfiles will be deleted automatically by other means. Session $sid SLAVE set registered [dlist_procedures $sid] set available [dlist_avail_procs $sid] set name [dict get $arguments NAME] if {$name in $registered} { lassign [procedures getproc $name] arg body ;# checks it exists. fileutil::writeFile $tempfile "proc $name {$arg} {$body}" ptyexec vi $tempfile seval [list $SLAVE invokehidden source $tempfile] procedures register $sid $name ;# update } elseif {$name in $available} { set arg [seval [list $SLAVE invokehidden info args $name]] set body [seval [list $SLAVE invokehidden info body $name]] fileutil::writeFile $tempfile "proc $name {$arg} {$body}" ptyexec vi $tempfile seval [list $SLAVE invokehidden source $tempfile] } else { fileutil::writeFile $tempfile "proc $name {args} {\n}" ptyexec vi $tempfile seval [list $SLAVE invokehidden source $tempfile] } return } } ;# End of Namespace