~aleteoryx/tclircc

c1e9562b7a4dc4f00d93c17ac50b8697b613582c — aleteoryx a month ago 6392294
plugin loading prompts
5 files changed, 222 insertions(+), 13 deletions(-)

M db/main.tcl
M migrate_core.tcl
M plugins.tcl
M testplugin/manifest.tcl
M threads.tcl
M db/main.tcl => db/main.tcl +9 -0
@@ 27,3 27,12 @@ proc path_to_core {} {
  global data_dir
  return [file join $data_dir core.db]
}

proc backup {} {
  global log data_dir
  set backup_dir [string cat $data_dir _backup_ [clock microseconds]]
  ${log}::info "backing up data directory to $backup_dir..."
  file copy $data_dir $backup_dir
  ${log}::info "backup to $backup_dir complete!"
  return $backup_dir
}

M migrate_core.tcl => migrate_core.tcl +3 -2
@@ 8,8 8,9 @@ migrator eval {
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
                      manifest_hash TEXT NOT NULL, -- sha256
                      trusted_until TEXT NOT NULL, -- ignore manifest changes if loading before timestamp
                      updated_on TEXT NOT NULL, -- date of last update
                      priority INTEGER NOT NULL, -- load priority
                      PRIMARY KEY (slug, namespace));
  }

M plugins.tcl => plugins.tcl +158 -9
@@ 34,7 34,7 @@ namespace eval plugins {
    version {$val != ""} }
  variable mf_optional {
    description 1
    author 1
    authors 1
    license 1 }
  variable mf_procs {
    


@@ 102,7 102,7 @@ namespace eval plugins {

  proc enroll_interp {tid iid} {}

  proc load {dir {allow_internal 0}} {
  proc load {dir {skip_prompt 0}} {
    variable log
    variable manifest_reader



@@ 170,22 170,171 @@ namespace eval plugins {
    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 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 hashes, version, trusted FROM plugins
      SELECT manifest_hash, version, trusted_until FROM plugins
        WHERE slug = $pl_slug
          AND namespace = $pl_namespace}]

#    load_license manifest

    if ![llength $stored] {
      puts "new plugin"
      tailcall int_load_new $skip_prompt $manifest $dir
    } else {
      lassign $stored st_hashes st_version st_trusted
      lassign $stored st_mf_hash st_version st_trusted_till
      switch -- [vercmp $pl_version $st_version] {
        0 { puts "same version" }
       -1 { puts "older version" }
        1 { puts "newer 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} {

  }
}

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

set authors {Aleteoryx <alyx@aleteoryx.me>}
set license spdx:WTFPL

M threads.tcl => threads.tcl +48 -1
@@ 5,6 5,7 @@ namespace eval threads {
  variable t_ns_script {
    namespace eval ::t {
      variable ns
      variable self
      proc update {} {
        variable ns
        thread::send -head [set ns(main)] threads::update


@@ 17,9 18,55 @@ namespace eval threads {
        variable log
        parray t::ns
      }
      proc util_pctsub {script} {
        set new_script {}
        set idx 0
        while {[set subst_start [string first "%" $script $idx]] != -1} {
          append new_script [string range $script $idx $subst_start-1]
          if {[string index $script $subst_start+1] == "%"} {
            append new_script %
            set idx [expr {$subst_start + 2}]
            continue
          }
          regexp -start [expr {$subst_start + 1}] -indices {[^A-Za-z0-9_:]} $script match
          if {[llength $match]} {
            lassign $match subst_end
            set subst_end $subst_end-1
          } else {
            set subst_end [string length $script]
          }
          upvar [string range $script $subst_start+1 $subst_end] subst_data
          append new_script $subst_data
          set idx [expr $subst_end + 1]
        }
        append new_script [string range $script $idx end]

        return $new_script
      }
      proc exec {name script {followup {}}} {
        variable ns
        variable self

        set script [uplevel [list ::t::util_pctsub $script]]
        set followup [uplevel [list ::t::util_pctsub $followup]]

        if [string length $followup] {
          set realscript {
            set result [eval %script]
            set script [concat [list set result $result] ";" %followup]
            thread::send -async [t::ns %self] $script
          }
          regsub %self $realscript [list $self] realscript
          regsub %followup $realscript [list $followup] realscript
          regsub %script $realscript [list $script] script
        }

        thread::send -async [set ns($name)] $script
      }
    }
  }
  eval $t_ns_script
  set ::t::self "main"

  proc manage {tid name} {
    variable threads


@@ 28,7 75,7 @@ namespace eval threads {
    dict set threads $name $tid
    ${log}::debug "managing thread $tid as \"$name\""

    thread::send -head $tid $t_ns_script
    thread::send -head $tid [concat $t_ns_script ";" [list set ::t::self $name]]

    ::threads::update
  }