Sicherheit: report/screen maskieren Shell-Eingaben, Sperren privat und atomar
report setzt Zellwerte in <!befehl!> maskiert ein, screen verlangt gültige Variablennamen, lock/unlock nutzen /tmp/rdb-locks-<UID> mit flock, vilock sperrt per Hardlink. EDITOR darf Argumente haben. Doku und Tests ergänzt.
This commit is contained in:
+50
-13
@@ -185,44 +185,81 @@ Rdb.register('enter', 'Add rows to a table or list file without an editor', 'ent
|
||||
end
|
||||
|
||||
require 'digest'
|
||||
require 'shellwords'
|
||||
|
||||
# ---- locks -------------------------------------------------------------------
|
||||
|
||||
# Lock files live in a private per-user directory, not loose in the shared
|
||||
# /tmp, so other users can neither forge nor delete them.
|
||||
def lock_dir
|
||||
dir = File.join(Dir.tmpdir, "rdb-locks-#{Process.uid}")
|
||||
Dir.mkdir(dir, 0o700) unless File.directory?(dir)
|
||||
st = File.lstat(dir)
|
||||
Rdb.die("#{dir}: not a private directory of yours") if st.symlink? || st.uid != Process.uid || (st.mode & 0o077) != 0
|
||||
dir
|
||||
end
|
||||
|
||||
def lock_file(name)
|
||||
File.join(Dir.tmpdir, "L#{File.basename(name)}.#{Digest::MD5.hexdigest(File.expand_path(name))[0, 8]}")
|
||||
File.join(lock_dir, "L#{File.basename(name)}.#{Digest::MD5.hexdigest(File.expand_path(name))[0, 8]}")
|
||||
end
|
||||
|
||||
# Read-modify-write of a lock file under an exclusive flock, so two lock or
|
||||
# unlock calls cannot interleave. The block gets the held locks and returns
|
||||
# the new list of lines, or nil to leave the file alone.
|
||||
def with_lock_file(name)
|
||||
File.open(lock_file(name), File::RDWR | File::CREAT, 0o600) do |f|
|
||||
f.flock(File::LOCK_EX)
|
||||
held = f.read.lines(chomp: true).map { |l| l.split.map(&:to_i) }
|
||||
lines = yield held
|
||||
next unless lines
|
||||
|
||||
f.rewind
|
||||
f.truncate(0)
|
||||
f.write(lines.map { |l| "#{l}\n" }.join)
|
||||
end
|
||||
end
|
||||
|
||||
Rdb.register('lock', 'Lock a record or field of a file', 'lock filename processid from to indexfrom indexto') do |args|
|
||||
die 'usage: lock filename processid from to indexfrom indexto' if args.size < 4
|
||||
name, pid, from, to = args
|
||||
lf = lock_file(name)
|
||||
held = File.exist?(lf) ? File.readlines(lf, chomp: true).map { |l| l.split.map(&:to_i) } : []
|
||||
clash = held.find { |p, f, t| p != pid.to_i && f < to.to_i && from.to_i < t }
|
||||
clash = nil
|
||||
with_lock_file(name) do |held|
|
||||
clash = held.find { |p, f, t| p != pid.to_i && f < to.to_i && from.to_i < t }
|
||||
clash ? nil : held.map { |l| l.join(' ') } << args.drop(1).join(' ')
|
||||
end
|
||||
if clash
|
||||
warn "lock: #{name}: bytes #{from}-#{to} are locked by process #{clash[0]}"
|
||||
exit 1
|
||||
end
|
||||
File.open(lf, 'a') { |f| f.puts args.drop(1).join(' ') }
|
||||
end
|
||||
|
||||
Rdb.register('unlock', 'Unlock a record or field of a file', 'unlock tableorlist processid from to indexfrom indexto') do |args|
|
||||
die 'usage: unlock filename processid from to indexfrom indexto' if args.size < 4
|
||||
name = args.shift
|
||||
lf = lock_file(name)
|
||||
next unless File.exist?(lf)
|
||||
next unless File.exist?(lock_file(name))
|
||||
|
||||
keep = File.readlines(lf, chomp: true).reject { |l| l.split.first(3) == args.first(3) }
|
||||
keep.empty? ? File.delete(lf) : File.write(lf, keep.join("\n") + "\n")
|
||||
with_lock_file(name) do |held|
|
||||
held.map { |l| l.join(' ') }.reject { |l| l.split.first(3) == args.first(3) }
|
||||
end
|
||||
lf = lock_file(name)
|
||||
File.delete(lf) if File.zero?(lf)
|
||||
end
|
||||
|
||||
Rdb.register('vilock', 'Lock a table before editing, unlock afterward', 'vilock tableorlist') do |args|
|
||||
file = args.first or die 'usage: vilock tableorlist'
|
||||
locked = File.join(File.dirname(file), "LOCK#{File.basename(file)}")
|
||||
die "#{file} is locked or missing" if File.exist?(locked) || !File.exist?(file)
|
||||
die "#{file} is locked or missing" unless File.exist?(file)
|
||||
|
||||
File.rename(file, locked)
|
||||
# link fails if LOCK<file> exists, so exactly one caller gets the table
|
||||
# (File.exist? followed by rename would leave a race in between).
|
||||
begin
|
||||
system(ENV['EDITOR'] || 'vi', locked)
|
||||
File.link(file, locked)
|
||||
rescue Errno::EEXIST
|
||||
die "#{file} is locked or missing"
|
||||
end
|
||||
File.unlink(file)
|
||||
begin
|
||||
system(*Shellwords.split(ENV['EDITOR'] || 'vi'), locked)
|
||||
ensure
|
||||
File.rename(locked, file)
|
||||
end
|
||||
@@ -239,7 +276,7 @@ def edit_cells(head, cells)
|
||||
Tempfile.create('rdb-edit') do |f|
|
||||
f.write Rdb::Table.new(head, [cells], list: true).to_s
|
||||
f.flush
|
||||
system(ENV['EDITOR'] || 'vi', f.path)
|
||||
system(*Shellwords.split(ENV['EDITOR'] || 'vi'), f.path)
|
||||
t = Rdb::Table.parse(File.read(f.path))
|
||||
return nil if t.rows.empty?
|
||||
|
||||
|
||||
@@ -2,13 +2,17 @@
|
||||
|
||||
# Reports, forms and mailing lists.
|
||||
|
||||
require 'shellwords'
|
||||
|
||||
# Fill in a form for one row: <Column> is replaced by the value, <!command!>
|
||||
# by the output of the shell command, and <!cmd <Column>!> passes data to it.
|
||||
# Values inside a command are shell-escaped, so a cell can never add commands
|
||||
# of its own; they arrive as one word each (do not put them in quotes).
|
||||
def fill_form(form, head, row)
|
||||
values = head.zip(row).to_h { |h, v| [h.strip, v.to_s] }
|
||||
form.gsub(/<!(.*?)!>|<([^<>!\n]*)>/m) do
|
||||
if Regexp.last_match(1)
|
||||
cmd = Regexp.last_match(1).gsub(/<([^<>!\n]*)>/) { values.fetch(Regexp.last_match(1).strip, Regexp.last_match(0)) }
|
||||
cmd = Regexp.last_match(1).gsub(/<([^<>!\n]*)>/) { Shellwords.escape(values.fetch(Regexp.last_match(1).strip, Regexp.last_match(0))) }
|
||||
`#{cmd}`
|
||||
else
|
||||
name = Regexp.last_match(2).strip
|
||||
@@ -57,6 +61,8 @@ Rdb.register('screen', 'Convert a form into a screen-input shell program', 'scre
|
||||
reads << format('cursor %2d %2d ; %s;', n + 1, col, m[1].strip)
|
||||
else
|
||||
names << m[2].strip
|
||||
# the name becomes a shell variable in the generated program
|
||||
die "screen: '#{m[2].strip}' is not a valid shell variable name" unless names.last.match?(/\A[A-Za-z_]\w*\z/)
|
||||
reads << format('cursor %2d %2d ; read %s;', n + 1, col, m[2].strip)
|
||||
end
|
||||
out[m.begin(0), m[0].size] = ' ' * m[0].size
|
||||
|
||||
@@ -230,6 +230,7 @@ end
|
||||
|
||||
Rdb.register('testall', 'Test all /rdb programs in directory $RDB/demo', 'testall [ > testall.new ]') do |_args|
|
||||
ENV['PATH'] = "#{File.join(Rdb::ROOT, 'bin')}:#{ENV['PATH']}"
|
||||
ENV['EDITOR'] = 'true' # no real editor in an unattended run (vilock, update.rdb e)
|
||||
demo = File.join(ENV['RDB'] || Rdb::ROOT, 'demo')
|
||||
script = File.join(demo, 'testall.cmds')
|
||||
File.exist?(script) or die "#{script}: missing"
|
||||
|
||||
Reference in New Issue
Block a user