M main.tcl => main.tcl +2 -0
@@ 43,6 43,8 @@ source plugins.tcl
start_thread irc
start_thread ui
+plugins::load "$path[file separator]testplugin"
+
${log}::debug "opening initial window..."
thread::send [t::ns tclircc::ui] {mk_toplevel name; return $name} initial
${log}::debug "initial window opened: $initial"
M plugins.tcl => plugins.tcl +79 -3
@@ 1,16 1,92 @@
package require sha256
# plugin manifest format:
-# tcl script that must return the following
+# tcl script that must set the following variables at a minimum:
+# - name: string
+# - slug: 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]
proc load {dir} {
- set mf_fd [open "$dir[file separator]manifest.tcl"]
+ variable log
+
+ ${log}::info "attempting to load plugin from \"$dir\"..."
+
+ set mf_path "$dir[file separator]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
+ # 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] + 1}]
+ 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 has_required [interp eval mf_exec {expr {[info exists name] &&
+ [info exists slug] &&
+ [info exists version]}}]
+ if {!$has_required} {
+ ${log}::error "couldn't load plugin: missing required values in manifest"
+ return 0
+ }
+
+ set name [interp eval mf_exec {set name}]
+ set slug [interp eval mf_exec {set slug}]
+ set version [interp eval mf_exec {set version}]
+
+ ${log}::debug "manifest loaded: $name ($slug) v[join $version .]"
+ } result opts]
+ switch -- $untrusted_result {
+ 0 { return result } 2 { return 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 SANDBOX COMPROMISE!"
+ return 0
+ }
+ }
}
}
A testplugin/manifest.tcl => testplugin/manifest.tcl +3 -0
@@ 0,0 1,3 @@
+set name "test plugin"
+set slug "test"
+set version {0 0 1}