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 <https://aleteoryx.me>}
# {Alice P Hacker <aph@example.com>}}
# - 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
author 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 {allow_internal 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]
${log}::debug "manifest loaded: ${pl_name} (${pl_namespace}::${pl_slug}) v[join ${pl_version} .]"
set stored [core_db eval {
SELECT hashes, version, trusted FROM plugins
WHERE slug = $pl_slug
AND namespace = $pl_namespace}]
if ![llength $stored] {
puts "new plugin"
} else {
lassign $stored st_hashes st_version st_trusted
switch -- [vercmp $pl_version $st_version] {
0 { puts "same version" }
-1 { puts "older version" }
1 { puts "newer version" }
}
}
}
}