#!/usr/bin/env ruby
# a direct request is just passed on
#
# exaclient --direct --request=somerequest.exa --result=somefile.pdf
#
# in an extended request the filename in the template file is replaced by the filename
# given on the command line; templates are located on the current path and at parent
# directories (two levels); the filename is expanded to a full path
#
# exaclient --extend --template=tmicare-l-h.exa --file=somefile.xml --result=somefile.pdf
#
# a constructed request is build out of the provided filename and action; the filename is
# expanded to a full path
#
# exaclient --construct --action=tmicare-s-h.exa --file=somefile.xml --result=somefile.pdf
#
# in all cases, the result is either determined by a switch or taken from a reply file
banner = ['WWWClient', 'version 1.0.0', '2003-2006', 'PRAGMA ADE/POD']
$: << File.expand_path(File.dirname($0)) ; $: << File.join($:.last,'lib') ; $:.uniq!
require 'base/switch'
require 'base/logger'
require 'timeout'
require 'thread'
require 'rexml/document'
require 'net/http'
class File
def File.backtracked(filename,level=3)
if level > 0 && filename && ! filename.empty? then
if FileTest.file?(filename) then
filename
else
File.backtracked('../'+filename,level-1)
end
else
filename
end
end
def File.expanded(filename)
File.expand_path(filename)
end
end
class Commands
include CommandBase
end
class Commands
@@namespace = "xmlns:exa='http://www.pragma-ade.com/schemas/example.rng'"
@@randchars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "0123456789" + "abcdefghijklmnopqrstuvwxyz"
def traceback
"(error: #{$!})" + "\n -- " + $@.join("\n >>")
end
def pdf(action,filename,enabled)
if enabled && FileTest.file?(filename) then
begin
report("pdf action #{action} on #{filename}")
case action
when 'close' then system("pdfclose --all")
when 'open' then system("pdfopen --file #{filename}")
end
rescue
# forget about it
end
end
end
def status(replyfile,str) # when block, then ok
begin
# def status(*whatever)
# end
File.open(replyfile,'w') do |f|
report("saving reply info in '#{replyfile}'")
f.puts("\n\n")
f.puts("\n")
if block_given? then
f.puts(" ok\n")
f.puts(" #{yield}\n")
else
f.puts(" error\n")
end
f.puts(" " + str + "\n")
f.puts("\n")
f.close
report("saving status: #{str}")
end
rescue
report("saving reply info in '#{replyfile}' fails")
ensure
exit
end
exit # to be real sure
end
def boundary_string (length) # copied from webrick/utils
rand_max = @@randchars.size
ret = ""
length.times do
ret << @@randchars[rand(rand_max)]
end
ret.upcase
end
end
class Commands
@@connecttimeout = 10*60 # ten minutes
@@processtimeout = 60*60 # an hour
@@polldelay = 5 # 5 seconds
def main
datatemplate = @commandline.option('template')
datafile = @commandline.option('file')
dataaction = @commandline.option('action')
if ! datatemplate.empty? then
report("template '#{datatemplate}' specified without --construct")
report("aborting")
elsif ! dataaction.empty? then
report("action data '#{dataaction}' specified without --construct or --extend")
report("aborting")
elsif ! datafile.empty? then
report("action file '#{datafile}' specified without --construct or --extend")
report("aborting")
else
report("assuming --direct")
direct()
end
end
def construct
requestfile = @commandline.option('request')
replyfile = @commandline.option('reply')
datatemplate = @commandline.option('template')
datafile = @commandline.option('file')
dataaction = @commandline.option('action')
domain = @commandline.option('domain')
project = @commandline.option('project')
username = @commandline.option('username')
password = @commandline.option('password')
threshold = @commandline.option('threshold')
datablob = ''
begin
datablob = IO.read(datatemplate)
rescue
datablob = ''
else
begin
request = REXML::Document.new(datablob)
if e = REXML::XPath.match(request.root,"/exa:request/exa:data") then
datablob = e.to_s.chomp
end
rescue
datablob = ''
end
end
begin
File.open(requestfile,'w') do |f|
f.puts "\n"
f.puts "\n"
f.puts " \n"
f.puts " #{dataaction}\n" unless dataaction.empty?
f.puts " #{datafile}\n" unless datafile.empty?
f.puts " #{threshold}\n" unless threshold.empty?
f.puts " \n"
f.puts " \n"
f.puts " #{domain}\n"
f.puts " #{project}\n"
f.puts " #{username}\n"
f.puts " #{password}\n"
f.puts " \n"
if datablob.empty? then
f.puts " \n"
else
f.puts " #{datablob.chomp}\n"
end
f.puts ""
end
rescue
status(replyfile,"unable to create '#{requestfile}'")
end
direct()
end
def extend
requestfile = @commandline.option('request')
replyfile = @commandline.option('reply')
datatemplate = @commandline.option('template')
datafile = @commandline.option('file')
dataaction = @commandline.option('action')
threshold = @commandline.option('threshold')
if datatemplate.empty? then
status(replyfile,"invalid data template '#{datatemplate}'")
else
begin
if FileTest.file?(datatemplate) && oldrequest = IO.read(datatemplate) then
request, done = REXML::Document.new(oldrequest), false
if ! threshold.empty? && e = REXML::XPath.match(request.root,"/exa:request/exa:application/exa:threshold") then
e.text, done = threshold, true
end
if ! dataaction.empty? && e = REXML::XPath.match(request.root,"/exa:request/exa:application/exa:action") then
e.text, done = dataaction, true
end
if ! datafile.empty? && e = REXML::XPath.match(request.root,"/exa:request/exa:application/exa:filename") then
e.text, done = datafile, true
end
#
if ! threshold.empty? && e = REXML::XPath.match(request.root,"/exa:request/exa:application") then
e = e.add_element('exa:threshold')
e.add_text(threshold.to_s)
done = true
end
#
report("nothing replaced in template file") unless done
begin
File.open(requestfile,'w') do |f|
f.puts(newrequest.to_s)
end
rescue
status(replyfile,"unable to create '#{requestfile}'")
end
else
status(replyfile,"unable to read data template '#{datatemplate}'")
end
rescue
status(replyfile,"unable to handle data template '#{datatemplate}'")
end
end
direct()
end
def direct
requestpath = @commandline.option('path')
requestfile = @commandline.option('request')
replyfile = @commandline.option('reply')
resultfile = @commandline.option('result')
datatemplate = @commandline.option('template')
datafile = @commandline.option('file')
threshold = @commandline.option('threshold')
address = @commandline.option('address')
port = @commandline.option('port')
session_id = @commandline.option('session')
exaurl = @commandline.option('exaurl')
exaurl = "/#{exaurl}" unless exaurl =~ /^\//
address.sub!(/^http\:\/\//io) do
''
end
address.sub!(/\:(\d+)$/io) do
port = $1
''
end
autopdf = @commandline.option('autopdf')
dialogue = nil
resultfile.sub!(/\.[a-z]+?$/, '') # don't overwrite the source
unless requestpath.empty? then
begin
if FileTest.directory?(requestpath) then
if Dir.chdir(requestpath) then
report("gone to path '#{requestpath}'")
else
status(replyfile,"unable to go to path '#{requestpath}")
end
else
status(replyfile,"unable to locate '#{requestpath}'")
end
rescue
status(replyfile,"unable to handle '#{requestpath}'")
end
end
datafile = File.expand_path(datafile) unless datafile.empty?
datatemplate = File.backtracked(datatemplate,3) unless datatemplate.empty?
# request must be valid
status(replyfile,'no request file') if requestfile.empty?
status(replyfile,"invalid request file '#{requestfile}'") unless FileTest.file?(requestfile)
begin
request = IO.readlines(requestfile).join('')
request = REXML::Document.new(request)
status(replyfile,'invalid request (no request)') unless request.root.fully_expanded_name=='exa:request'
status(replyfile,'invalid request (no application block)') unless request.elements['exa:request'].elements['exa.application'] == nil # explicit nil test needed
rescue REXML::ParseException
status(replyfile,'invalid request (invalid xml file)')
rescue
status(replyfile,'invalid request (invalid file)')
else
report("using request file '#{requestfile}'")
end
# request can force session_id
if session_id && session_id.empty? then
begin
id = request.elements['exa:request'].elements['exa:application'].elements['exa:session'].text
rescue Exception
id = ''
ensure
if id && ! id.empty? then
session_id = id
end
end
end
# request can overload reply name
begin
rreplyfile = request.elements['exa:request'].elements['exa:application'].elements['exa:output'].text
rescue Exception
rreplyfile = nil
ensure
if rreplyfile && ! rreplyfile.empty? then
replyfile = rreplyfile
report("reply file '#{replyfile} set by request'")
else
report("using reply file '#{replyfile}'")
end
end
# request can overload result name
begin
rresultfile = request.elements['exa:request'].elements['exa:application'].elements['exa:result']
rescue Exception
rresultfile = nil
ensure
if rresultfile && ! rresultfile.empty? then
resultfile = rresultfile
report("result file '#{resultfile}' set by request")
else
report("using result file '#{resultfile}'")
end
end
# try to connect to server
start_time = Time.now
processtimeout = begin @commandline.option('timeout').to_i rescue @@processtimeout end
processtimeout = @@processtimeout if processtimeout == 0 # 'xx'.to_i => 0
dialogue = start_dialogue(address, port, processtimeout)
if dialogue then
# continue
else
status(replyfile,'no connection')
end
# post request
timeout (@@processtimeout-10) do # -10 so that we run into this one first
begin
report("posting request of type '#{exaurl}'")
report("using session id '#{session_id}'") if session_id && ! session_id.empty?
firstline, chunks, total = nil, 0, 0
body, boundary, crlf = '', boundary_string(32), "\x0d\x0a"
body << '--' + boundary + crlf
body << "Content-Disposition: form-data; name=\"exa:request\""
body << crlf
body << "Content-Type: text/plain"
body << crlf + crlf
body << request.to_s
body << crlf + '--' + boundary + crlf
if session_id && ! session_id.empty? then
body << "Content-Disposition: form-data; name=\"exa:session\""
body << "Content-Type: text/plain"
body << crlf + crlf
body << session_id
body << crlf + '--' + boundary + crlf
end
begin
File.open(datafile,'rb') do |df|
body << "Content-Disposition: form-data; name=\"filename\""
body << "Content-Type: text/plain"
body << crlf + crlf
body << datafile
body << crlf + '--' + boundary + crlf
body << "Content-Disposition: form-data; name=\"fakename\" ; filename=\"#{datafile}\""
body << "Content-Type: application/octetstream"
body << "Content-Transfer-Encoding: binary"
body << crlf + crlf
body << df.read
body << crlf + '--' + boundary + '--' + crlf
end
rescue
# skip
end
headers = Hash.new
headers['content-type'] = "multipart/form-data; boundary=#{boundary}"
headers['content-length'] = body.length.to_s
begin
File.open(resultfile,'wb') do |rf|
begin
# firstline is max 1024 but ok for reply
dialogue.post(exaurl,body,headers) do |str|
if ! firstline || firstline.empty? then
report('receiving result') if total == 0
firstline = str
end
total += 1
rf.write(str)
end
rescue
report("forced close #{traceback}")
end
end
rescue
status(replyfile,'cannot open file')
end
begin
File.delete(resultfile) if File.zero?(resultfile)
rescue
end
unless FileTest.file?(resultfile) then
report("deleting empty resultfile")
begin
File.delete(resultfile)
rescue
# nice try, an error anyway
end
status(replyfile,'empty file')
else
n, id, status = 0, '', ''
loop do
again = false
if ! dialogue then
again = true
elsif firstline =~ /(\ 1
unless dialogue then
report('reestablishing connection')
dialogue = start_dialogue(address, port, processtimeout)
end
if dialogue then
begin
File.open(resultfile,'wb') do |rf|
begin
body = "id=#{id}"
headers = Hash.new
headers['content-type'] = "application/x-www-form-urlencoded"
headers['content-length'] = body.length.to_s
total, firstline = 0, ''
dialogue.post("/exastatus",body,headers) do |str|
if ! firstline || firstline.empty? then
firstline = str
end
total += 1
rf.write(str)
end
rescue
report("forced close #{traceback}")
dialogue = nil
again = true
end
end
begin
File.delete(resultfile) if File.zero?(resultfile)
rescue
end
rescue
report("error in opening file #{traceback}")
status(replyfile,'cannot open file')
end
else
report("unable to make a connection")
status(replyfile,'unable to make a connection') # exit
end
else
break
end
end
case firstline
when /<\?xml\s*version=.*?\?>\s*#{resultfile}"
end
when /html/io then
report("done, file #{resultfile}, type html, #{total} chunks, #{File.size? rescue 0} bytes")
if resultfile =~ /\.(htm|html)$/i then
report("file identified as 'html'")
elsif resultfile =~ /\..*$/o
report("result file suffix should be 'htm'")
else
newresultfile = resultfile + '.htm'
begin
File.delete(newresultfile) if FileTest.file?(newresultfile)
resultfile = newresultfile if File.rename(resultfile,newresultfile)
rescue
report("adding 'htm' suffix to result name failed")
else
report("'htm' suffix added to result name")
end
end
report("result saved in '#{resultfile}'")
status(replyfile,'ok') do
"#{resultfile}"
end
else
report("no result file, first line #{firstline}")
status(replyfile,'no result file')
end
end
rescue TimeoutError
report("aborted due to time out")
status(replyfile,'time out')
rescue
report("aborted due to some problem #{traceback}")
status(replyfile,"no answer #{traceback}")
end
end
begin
report("run time: #{Time.now-start_time} seconds")
rescue
end
end
def start_dialogue(address, port, processtimeout)
timeout(@@connecttimeout) do
report("trying to connect to #{address}:#{port}")
begin
begin
if dialogue = Net::HTTP.new(address, port) then
# dialogue.set_debug_output $stderr
dialogue.read_timeout = processtimeout # set this before start
if dialogue.start then
report("connected to #{address}:#{port}, timeout: #{processtimeout}")
else
retry
end
else
retry
end
rescue
sleep(2)
retry
else
return dialogue
end
rescue TimeoutError
return nil
rescue
return nil
end
end
end
end
logger = Logger.new(banner.shift)
commandline = CommandLine.new
commandline.registerflag('autopdf')
commandline.registervalue('path' , '')
commandline.registervalue('request' , 'request.exa')
commandline.registervalue('reply' , 'reply.exa')
commandline.registervalue('result' , 'result')
commandline.registervalue('template' , '')
commandline.registervalue('file' , '')
commandline.registervalue('action' , '')
commandline.registervalue('timeout' , '')
commandline.registervalue('domain' , 'default')
commandline.registervalue('project' , 'default')
commandline.registervalue('username' , 'guest')
commandline.registervalue('password' , 'anonymous')
commandline.registervalue('exaurl' , 'exarequest')
commandline.registervalue('threshold' , '0')
commandline.registervalue('session' , '')
commandline.registervalue('address' , 'localhost')
commandline.registervalue('port' , '80')
commandline.registeraction('direct' , '[--path --request --reply --result --autopdf]')
commandline.registeraction('construct', '[--path --request --reply --result --autopdf] --file --action')
commandline.registeraction('extend' , '[--path --request --reply --result --autopdf] --file --action --template')
commandline.registeraction('direct')
commandline.registeraction('construct')
commandline.registeraction('extend')
commandline.registerflag('verbose')
commandline.registeraction('help')
commandline.registeraction('version')
commandline.expand
Commands.new(commandline,logger,banner).send(commandline.action || 'main')