~aleteoryx/tclircc

63922942ea1eb76534810686b711a550e3745c57 — aleteoryx a month ago 5363eb2
plugin version comparison, improved manifest reading
3 files changed, 119 insertions(+), 19 deletions(-)

M migrate_core.tcl
M plugins.tcl
M testplugin/manifest.tcl
M migrate_core.tcl => migrate_core.tcl +7 -5
@@ 5,11 5,13 @@ interp create migrator
migrator eval {
  lappend migrations 0 {init plugins} {
-- init plugins
CREATE TABLE plugins (slug TEXT PRIMARY KEY,
                      version INTEGER,
                      hashes TEXT,
                      trusted BOOL,
                      priority INTEGER);
CREATE TABLE plugins (slug TEXT NOT NULL, -- corresponds directly to manifest
                      namespace TEXT NOT NULL, -- corresponds directly to manifest
                      version TEXT NOT NULL, -- corresponds directly to manifest
                      hashes TEXT NOT NULL, -- dict of path -> hash
                      trusted BOOL NOT NULL, -- bypass security checks for plugins with this name
                      priority INTEGER NOT NULL, -- load priority
                      PRIMARY KEY (slug, namespace));
  }
}


M plugins.tcl => plugins.tcl +111 -14
@@ 5,6 5,8 @@ package require sha256
# - 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:


@@ 24,10 26,85 @@ 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} {
  proc load {dir {allow_internal 0}} {
    variable log
    variable manifest_reader

    ${log}::info "attempting to load plugin from \"$dir\"..."



@@ 43,6 120,7 @@ namespace eval plugins {
    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


@@ 51,7 129,7 @@ namespace eval plugins {
    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 time -seconds [expr {[clock seconds] + 2}]
    interp limit mf_exec command -value 10000

    # untrusted code time


@@ 63,22 141,18 @@ namespace eval plugins {
        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"
      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
      }

      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 .]"
      # hooray, we have the manifest!
      set manifest $result
      interp delete mf_exec
    } result opts]
    switch -- $untrusted_result {
      0 { return result } 2 { return result }
      0 {  } 2 { return -code 2 result }
      1 {
        ${log}::error "couldn't load plugin: $result"
        return 0


@@ 86,9 160,32 @@ namespace eval plugins {
      default {
        ${log}::alert "unexpected return code from plugin handling: $untrusted_result"
        ${log}::alert "return options: $opts"
        ${log}::alert "THIS MAY INDICATE A SANDBOX COMPROMISE!"
        ${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" }
      }
    }
  }
}

M testplugin/manifest.tcl => testplugin/manifest.tcl +1 -0
@@ 1,3 1,4 @@
set name "test plugin"
set slug "test"
set namespace "aleteoryx"
set version {0 0 1}