#*** # [manpage_begin plugins tclircc 0.0.1] # [titledesc {Component plugins.tcl}] # [description] package require sha256 # plugin manifest format: # tcl script that must set the following variables at a minimum: # - name: string # - slug: string # must match /^[-_a-zA-Z0-9]+$/ # - namespace: string # must match /^[-_a-zA-Z0-9]+$/ # - version: string[] # ordered like {1} > {0}, {0 1} < {1 0}. {1 1} > {1} # may set: # - description: string # - author: string[] # should contain a list of names with optional website or email, e.g. # {{Aleteoryx } # {Alice P Hacker }} # - license: string # may begin with spdx: to indicate an spdx id or file: to indicate # a file path relative to the plugin root. .. and root references are # stripped. if neither prefix applies, assumed to be literal license # text. namespace eval plugins { variable plugins variable log [logger::init tclircc::plugins] # generate the script to exfiltrate data from the manifest variable mf_required { name 1 namespace {[regexp {^[_a-zA-Z0-9-]+$} $val]} slug {[regexp {^[_a-zA-Z0-9-]+$} $val]} version {$val != ""} } variable mf_optional { description 1 authors 1 license 1 } variable mf_procs { } variable manifest_reader { set manifest_dict {} } foreach {key check} $mf_required { set chunk { if {[info exists %key]} { set val [set %key] if %check { dict set manifest_dict %key [set %key] } else { return -code error "invalid value for %key: $val" } } else { return -code error "missing key in manifest: %key" } } regsub -all "%key" $chunk [list $key] chunk regsub -all "%check" $chunk [list $check] chunk append manifest_reader $chunk } foreach {key check} $mf_optional { set chunk { if {[info exists %key]} { set val [set %key] if %check { dict set manifest_dict %key [set %key] } } } regsub -all "%key" $chunk [list $key] chunk regsub -all "%check" $chunk [list $check] chunk append manifest_reader $chunk } foreach {key _} $mf_procs { set chunk { if {%key in [info procs %key]} { dict set manifest_dict procs %key [list [info args %key] [info body %key]] } } regsub -all "%key" $chunk [list $key] chunk append manifest_reader $chunk } append manifest_reader { set manifest_dict } # generation done proc vercmp {v1 v2} { # normalize the lists, check for equality if {[list {*}$v1] == [list {*}$v2]} { return 0 } # compare every element for {set n 0} {$n < min([llength $v1], [llength $v2])} {incr n} { if [set cmp [string compare [lindex $v1 $n] [lindex $v2 $n]]] { return $cmp } } # they can't be equal, so the longer one must be the newer one if {[llength $v1] > [llength $v2]} { return 1 } return -1 } proc enroll_interp {tid iid} {} proc load {dir {skip_prompt 0}} { variable log variable manifest_reader ${log}::info "attempting to load plugin from \"$dir\"..." set mf_path [file join $dir manifest.tcl] if {!([file isfile $mf_path] && [file readable $mf_path])} { ${log}::error "couldn't load plugin: manifest \"$mf_path\" not present or not readable!" return 0 } set mf_fd [open $mf_path] set manifest [read $mf_fd] close $mf_fd interp create -safe mf_exec # prevent escapes interp hide mf_exec package interp hide mf_exec interp # prevent hanging the thread interp hide mf_exec vwait interp hide mf_exec after interp hide mf_exec update interp hide mf_exec gets interp hide mf_exec read # this is more than enough for any reasonable manifest interp limit mf_exec time -seconds [expr {[clock seconds] + 2}] interp limit mf_exec command -value 10000 # untrusted code time ${log}::debug "evaluating manifest..." set untrusted_result [catch { set mf_result [catch { interp eval mf_exec $manifest } result opts] if {$mf_result != 0} { ${log}::error "couldn't load plugin: manifest return code $mf_result: $result" return 0 } set mf_valid [catch { interp eval mf_exec $manifest_reader } result opts] if {$mf_valid != 0} { ${log}::error "couldn't validate plugin: $result" return 0 } # hooray, we have the manifest! set manifest $result interp delete mf_exec } result opts] switch -- $untrusted_result { 0 { } 2 { return -code 2 result } 1 { ${log}::error "couldn't load plugin: $result" return 0 } default { ${log}::alert "unexpected return code from plugin handling: $untrusted_result" ${log}::alert "return options: $opts" ${log}::alert "THIS MAY INDICATE A PLUGIN SANDBOX COMPROMISE!" return 0 } } set pl_name [dict get $manifest name] set pl_namespace [dict get $manifest namespace] set pl_slug [dict get $manifest slug] set pl_version [dict get $manifest version] set pl_qslug "${pl_namespace}::${pl_slug}" set pl_verstr "v[join $pl_version .]" # qualified slug dict set manifest qslug $pl_qslug dict set manifest verstr $pl_verstr ${log}::debug "manifest loaded: $pl_name (${pl_qslug}) $pl_verstr" set stored [core_db eval { SELECT manifest_hash, version, trusted_until FROM plugins WHERE slug = $pl_slug AND namespace = $pl_namespace}] # load_license manifest if ![llength $stored] { tailcall int_load_new $skip_prompt $manifest $dir } else { lassign $stored st_mf_hash st_version st_trusted_till switch -- [vercmp $pl_version $st_version] { 0 { tailcall int_load_existing $manifest $dir $mf_path $st_mf_hash $st_trusted_till } 1 { tailcall int_load_updated $skip_prompt $manifest $dir $st_version $st_trusted_till } -1 { tailcall int_load_old $manifest $dir $st_version } } } } proc int_load_new {skip_prompt manifest dir} { variable log ${log}::info "installing new plugin [dict get $manifest qslug]" if {!$skip_prompt} { set manifest [list $manifest] set dir [list $dir] t::exec tclircc::ui { set manifest %manifest tk_messageBox -type yesno \ -title "Plugin Installation" \ -icon "question" \ -message "Do you want to install \"[dict get $manifest name]\"?" \ -detail [string cat \ "[dict get $manifest qslug] [dict get $manifest verstr]" \ "\nBy [join [dict get $manifest authors] ,]." \ "\n\n[dict get $manifest license]" ] } { set manifest %manifest switch -- $result { yes { ::plugins::install $manifest %dir } no { set log %log ${log}::info "user cancelled installation of plugin [dict get $manifest qslug]" } } } } else { install $manifest $dir } } proc int_load_existing {manifest dir mf_path mf_hash trusted_till} { variable log ${log}::debug "loading existing plugin [dict get $manifest qslug]" if {$trusted_till >= [clock seconds]} { install $manifest $dir } elseif {[sha2::sha256 -file $mf_path] == $mf_hash} { install $manifest $dir } else { ${log}::warn "plugin may have been tampered with!" set manifest [list $manifest] set dir [list $dir] t::exec tclircc::ui { set manifest %manifest tk_messageBox -type yesno \ -title "Plugin Tampering Detected!" \ -icon "warning" \ -message "\"[dict get $manifest name]\" ([dict get $manifest qslug]) has been edited since last load!" \ -detail [string cat \ "This may indicate a security compromise, do you want to continue loading it?" \ " (If you are doing plugin development, set the \"Trusted Until\" option!)"] } { set manifest %manifest set log %log switch -- $result { yes { ${log}::warn "loading [dict get $manifest qslug] despite potential tampering!" ::plugins::install $manifest %dir } no { ${log}::info "not loading [dict get $manifest qslug] due to tampering" } } } } } proc int_load_updated {skip_prompt manifest dir version trusted_till} { variable log ${log}::info "updating plugin [dict get $manifest qslug] (v[join $version .] -> [dict get $manifest verstr])" if {!$skip_prompt && [clock seconds] > $trusted_till} { set manifest [list $manifest] set dir [list $dir] t::exec tclircc::ui { set manifest %manifest tk_messageBox -type yesno \ -title "Plugin Update" \ -icon "question" \ -message "Do you want to update \"[dict get $manifest name]\"?" \ -detail [string cat \ "[dict get $manifest qslug] [dict get $manifest verstr]" \ "\nBy [join [dict get $manifest authors] ,]." \ "\n\n[dict get $manifest license]" ] } { set manifest %manifest switch -- $result { yes { ::plugins::install $manifest %dir } no { set log %log ${log}::info "user cancelled update of plugin [dict get $manifest qslug]" } } } } else { install $manifest $dir } } proc int_load_old {manifest dir version} { variable log ${log}::warn "attempting to load older version of [dict get $manifest qslug] (v[join $version .] -> [dict get $manifest verstr])" set verstr [list "v[join $version .]"] set manifest [list $manifest] set dir [list $dir] t::exec tclircc::ui { set manifest %manifest tk_messageBox -type yesno \ -title "Plugin Downgrade!" \ -icon "warning" \ -message [string cat \ "Are you sure you want to downgrade \"[dict get $manifest name]" \ "\" ([dict get $manifest qslug])? A database backup will be made." ] \ -detail [string cat \ "Last saw " %verstr ", attempting to load [dict get $manifest verstr]."] } { set manifest %manifest set log %log switch -- $result { yes { ${log}::warn "downgrading [dict get $manifest qslug]! making a backup!" t::exec tclircc::db backup { ::plugins::install $manifest %dir } } no { ${log}::info "not downgrading [dict get $manifest qslug]" } } } } proc install {manifest dir} { } } #*** # [manpage_end]