#!/usr/bin/ruby # netstiff (formerly known as webdiff) # (C) 2004,2007, Stephan Beyer, GNU GPL # # requirements: # * ruby standard distribution (net::http, uri, pstore) # http://www.ruby-lang.org (at least version 1.8) # recommended: # * GNU readline for ruby (for interactive configuration) # wishlist/todo? # * a get-and-diff-at-once ("fastcheck"/"fetchdiff") command [requested] # Please tell me if you find any bugs: Stephan Beyer # You may also tell me your thoughts/ideas/opinion about this tool. ProgName = 'netstiff' ProgVersion = '20070621' DefaultTimeout = 20 DefaultTest = 'diff' DefaultContext = 3 DefaultPassiveFTP = 'true' $workdir = ENV['HOME']+'/.'+ProgName+'/' # this looks stupid, but it's needed for something like # netstiff -S -notexisting $stderr = $stderr.dup $errors = 0 def showversion puts "#{ProgName} #{ProgVersion}\t\t(C) 2004, 2007 Stephan Beyer, GNU GPL" end def showusage puts <<-EOU Usage: #{$0} [options] [command] Command can be one of: configure configure #{ProgName} diff show the current differences get fetch data from URIs and show changed ones help show this help reset reset current differences version show current version `get' is used, if command is omitted. Commands may be shortened down to one character (e.g. `c' instead of `configure'). Leading dashes are ignored. Without configuration, some commands will act like `configure'. Options: --no-stderr, -S suppress output on STDERR --workdir DIR, -W DIR specify working directory [#{$workdir}] See the manual page `man 1 #{ProgName}'. Have fun! EOU end def getcmd(c) c = c[1..-1] while c[0] == ?- command=['configure','diff','help','reset','version','get'] (c.length-1).downto(0) do |i| command.delete_if do |x| c[i] != x[i] end break if command.empty? end Options.err "Could not recognize command." if command.length != 1 command.pop end def name(test, url, suffix=nil) if suffix.nil? test+'::'+url else test+'::'+url+'::'+suffix end end $gnu = begin require 'readline' true rescue LoadError false end def readln(str = '') if $gnu Readline.readline(str, true) else print str STDIN.gets.chomp end end def str_to_re(str) eval('%r'+str) ## FIXME this is eval^Wevil! # How can this be fixed? # See this #ruby-lang log: # try yaml. # {:regex1 => /foo/i, :regex2 => /bar/u}.to_yaml # => "--- # \n:regex2: !ruby/regexp /bar/u\n:regex1: !ruby/regexp /foo/i\n" # regexen are slightly uncomfortable, but most stuff is human readable 1:1 # apeiros, ok, thank you, it's noticed. And is there a way if I don't want # to change the format? ;-) # the obvious: eval or split the expression and use Regexp.new end def errorout(str) $log.puts str $stderr.puts str end ## first some workarounds for used libs require 'net/ftp' module Net class FTP # first overwrite open and new to take ports! def FTP.open(host, port = FTP_PORT, user = nil, passwd = "", acct = nil) if block_given? ftp = new(host, port, user, passwd, acct) begin yield ftp ensure ftp.close end else new(host, port, user, passwd, acct) end end def initialize(host = nil, port = FTP_PORT, user = nil, passwd = "", acct = nil) super() @binary = true @passive = false @debug_mode = false @resume = false if host connect(host,port) if user if passwd.nil? login(user, "", acct) else login(user, passwd, acct) end end end end # a nice retr def retr(path) ret = '' retrbinary('RETR '+path, DEFAULT_BLOCKSIZE) do |buf| ret << buf end ret end # a nice listing def listing(path) ret = '' list(path) do |line| ret << line << "\n" end ret end #def sendcmd(cmd) # synchronize do # puts "DEBUG -- sending: \"#{cmd}\"" # putline(cmd) # return getresp # end #end end end module Options def Options.err(s) STDERR.puts "Error: #{s}\nAborting. See `#{$0} -h' for help." exit 1 end def Options.handle(opt,x,a) case opt when :W $workdir = x $workdir += '/' if $workdir[-1] != ?/ end end def Options.init # TODO: make code nicer. Just terrible! ;-) args = 0 opt = nil ARGV[0..-2].each do |z| if args.zero? case z when '--workdir', '-W' opt = :W args = 1 when '--no-stderr', '-S' def $stderr.write(x) end else err("Unknown option: #{z}") end else handle(opt,z,args) args -= 1 end end if args > 1 err("Too many arguments to option -#{opt} given.") exit 1 elsif args == 1 handle(opt,ARGV.last,args) 'g' elsif ARGV.length == 0 'g' elsif ARGV.last == '-S' or ARGV.last == '--no-stderr' def $stderr.write(x) end 'g' else ARGV.last end end end class Store require 'pstore' def initialize @db = PStore.new($workdir+'store') end def each @db.transaction do @db.roots.each do |*args| yield(*args) end end end def [](key) @db.transaction(true) do @db[key] end end def []=(key,value) @db.transaction do @db[key]=value end end def delete(key) @db.delete(key) end end class Config def initialize(use=true) @conf = [{}] begin readconfigfile use rescue SystemCallError errorout $! $log.puts('configuring') configure exit end end def each(&block) @conf.each(&block) end def [](key) @conf[key] end def readconfigfile(use) i = 0 lastcomment = '' File.open($workdir+'config', 'r') do |conffile| conffile.each_line do |line| line.strip! if line =~ /^#/ or line.empty? lastcomment = line elsif line =~ /^\+/ k, v = line[1..-1].strip.split(/\s+/, 2) @conf[i][k] = v lastcomment = '' else i += 1 @conf << {'url'=> ((line=~%r(^(https?|ftp)://)i)==0 ? '':'http://')+line} unless lastcomment.empty? @conf[i]['title'] = lastcomment.sub(/^#+ */, '') end end end end if use glob = @conf.shift # Default values overwritten? glob['test'] = DefaultTest if glob['test'].nil? glob['passive'] = DefaultPassiveFTP if glob['passive'].nil? glob['timeout'] = DefaultTimeout if glob['timeout'].nil? # Now set variables in every URI, if not set glob.each do |gvar,gval| next if gval.nil? @conf.each_with_index do |e,i| e[gvar] = gval if e[gvar].nil? end end end end def configure showversion print <<-EOD This is the #{ProgName} #{ProgVersion} interactive configuration tool. #{ProgName} uses a simple human-readable configuration file #{$workdir+'config'} but you can always use this tool by executing `#{$0} -c' on the command line. EOD if configure_mainmenu # save config File.open($workdir+'config', 'w') do |out| out.print <<-EOH # #{ProgName} configuration file, type `#{ProgName} -c' to configure # Format: # # comment # + # + ... # # + # + ... # # ... # # For HTTP URIs: # variables are: client, lang, proxy, referer, test, timeout, range # can be html, diff, size, date, md5sum or a /regexp/ # #{DefaultTest} is the default. # The proxy value must be in the format : # # 'diff'/'html' test modes have two extra variables: # start /start regex/ and end /end regex/ # These two are nice for dynamic web pages with a more interesting static # part. Set start and end regex, and then #{ProgName} only diffs the content # between the first 'start' and the last 'end' match, and not a change # like "xx users online" or a random (fortune) server signature. # # 'html' has a further extra variable: htmlcmd # e.g. " + htmlcmd lynx -nolist -dump" or " + htmlcmd html2text" # # For FTP URIs: # variables are: test, timeout, passive # can be diff, size, date # #{DefaultTest} is the default. # passive is set to #{DefaultPassiveFTP} by default. # # All variable settings are optional. # Have fun! sbeyer EOH @conf.each do |e| out.puts "\n" out.puts '# '+e['title'] unless e['title'].nil? out.puts e['url'] unless e['url'].nil? out.puts " + test\t"+e['test'] unless e['test'].nil? out.puts " + client\t"+e['client'] unless e['client'].nil? out.puts " + referer\t"+e['referer'] unless e['referer'].nil? out.puts " + lang\t"+e['lang'] unless e['lang'].nil? out.puts " + range\t"+e['range'] unless e['range'].nil? out.puts " + timeout\t"+e['timeout'] unless e['timeout'].nil? out.puts " + proxy\t"+e['proxy'] unless e['proxy'].nil? out.puts " + passive\t"+e['passive'] unless e['passive'].nil? out.puts " + htmlcmd\t"+e['htmlcmd'] if e['test']=='html' and not e['htmlcmd'].nil? if ['html','diff'].member?(e['test']) out.puts " + start\t"+e['start'] unless e['start'].nil? out.puts " + end \t"+e['end'] unless e['end'].nil? end end end end end def configure_mainmenu loop do cols = ENV['COLUMNS'].to_i cols = 80 if cols < 40 puts "\n\n" possible = ['n', 'f', 'x'] @conf.each_with_index do |e, c| print " [#{c}]\t" possible << c.to_s if c == 0 puts 'Global settings' else if e['title'].nil? u = e['url'] w1 = cols - 20 if u.length >= w1 w1,w2 = w1.divmod 2 u = u[0..w1+w2]+'...'+u[-1-w1..-1] end else u = e['title'] end puts u+' settings' end end puts " [n]\tAdd new URI" puts puts " [f]\tFinished (exit & save)" puts " [x]\tExit without saving" case (c = choose(possible)) when 'f' return true when 'x' return false when 'n' nr = @conf.length configure_url(nr) #configure_testmethod(nr) configure_urlmenu(nr) else configure_urlmenu(c.to_i) end end end def choose(possible, allowempty=false) puts choice = readln(" Your choice [#{possible.join('/')}]: ") until possible.member?(choice) or (allowempty and not choice.nil? and choice.empty?) puts "\n" choice end def ask_regexp puts "\n"+'Please type the regexp, e.g. /[Hh]ugo\s+wrote/ ...' begin re = readln('> ') str_to_re(re) rescue SyntaxError puts 'Incorrect regular expression! Try again:' retry end re end def configure_url(nr) puts "\nPlease type the correct URI of the web page you want to check for updates:" if @conf[nr].nil? @conf[nr] = {'url'=>readln('> ')} else @conf[nr]['url'] = readln('> ') end end def configure_testmethod(nr) if show(nr,'url')=~ %r(^ftp://)i puts <<-EOD Choose the TEST METHOD, the way of looking for changes. Type: diff This method downloads the whole file or the directory listing and saves the two last versions. Later you can use `#{ProgName} diff' to take a look at a diff of these versions. size This method requests the size of the file on the FTP site to check if it changes. date This method requests the last modification date of the file on the FTP site to check when the file was last modified. EOD x = choose(['diff','size','date'],true) @conf[nr]['test'] = x unless x.empty? else # HTTP puts <<-EOD Choose the TEST METHOD, the way of looking for changes. Type: diff This method downloads the whole file and saves the two last versions. Later you can use `#{ProgName} diff' to take a look at a diff of these versions. html This method acts like 'diff', but ignores text between '<' and '>' on diffing. (Those usually denote HTML tags.) size This method downloads the HTTP header to check if the file size changes. The server should response the Content-Length header entity. (works in most cases) date This method downloads the HTTP header to check when the file was last modified. This behaviour can cause problems when fetching some dynamic web sites. The server should response the Last-Modified header entity. (works in most cases) md5sum This method downloads the HTTP header to check if the MD5 sum changes. The server should response the Content-MD5 header entity to make this method work. regexp This method downloads the whole file and checks if a given regexp matches. Nothing is stored locally. EOD x = choose(['diff','html','size','date','md5sum','regexp'],true) @conf[nr]['test'] = x unless x.empty? @conf[nr]['test'] = ask_regexp if @conf[nr]['test'] == 'regexp' end end def emptydel(nr,v) if @conf[nr][v].empty? @conf[nr].delete v true else false end end def configure_useragent(nr) puts "\nType an User-Agent identification string:" @conf[nr]['client'] = readln('> ') emptydel nr,'client' end def configure_referer(nr) puts "\nType the suggested URI of the Referer[sic]:" @conf[nr]['referer'] = readln('> ') emptydel nr,'referer' end def configure_title(nr) puts "\nType a menu title for the URI:" @conf[nr]['title'] = readln('> ') emptydel nr,'title' end def configure_passive(nr) print "\nShould the FTP server's passive mode be requested?" x = choose(['y','n'],true) case x when 'y' @conf[nr]['passive'] = 'true' when 'n' @conf[nr]['passive'] = 'false' end end def configure_lang(nr) puts "\nType your language code, e.g de, ca or en:" @conf[nr]['lang'] = readln('> ') emptydel nr,'lang' end def configure_range(nr) # This option only makes sense when using the DIFF method puts puts 'Note: You should NOT use this option globally.' if nr==0 puts 'Not all HTTP servers support this feature.' puts <<-EOH Type a range. Examples: 0-99 = get the first 100 bytes (byte 0 to byte 99) -99 = get the last 99 bytes 99-99 = get the 99th byte 99- = get all bytes, beginning with the 99th 65-923 = get bytes 65 to 923 EOH begin @conf[nr]['range'] = readln('> ') end until (@conf[nr]['range'] =~ /^\d*-\d*$/) or @conf[nr]['range'].empty? emptydel nr,'range' end def configure_timeout(nr) puts "\nConnection timeout in seconds:" @conf[nr]['timeout'] = readln('> ') emptydel nr,'timeout' end def configure_proxy(nr) # TODO: yet HTTP-only puts "\nWhat is host and port of your HTTP proxy? Format: host:port" begin @conf[nr]['proxy'] = readln('http://') end until (@conf[nr]['proxy'] =~ /^[\w\.-]+:\d+$/) or @conf[nr]['proxy'].empty? emptydel nr,'proxy' end def configure_htmlcmd(nr) puts <<-EOH Please type a shell command to use a HTML dumping tool. Or type nothing to use the #{ProgName}-internal tag ignorer. Examples: > lynx -nolist -dump <- they > links2 -dump <- work > w3m -dump <- all > html2text <- well ;-) > cat ;-) <- Here, use the `diff' method instead. EOH @conf[nr]['htmlcmd'] = readln('> ') return if emptydel nr,'htmlcmd' puts <<-EOH Fine, I am doing a test now. I'll generate a simple HTML page and start the command you just entered. The output will be shown here. If you see that something went wrong, you should reenter this configuration part! Ready? Press a key. EOH data = <<-EOD #{ProgName} dump test

A dump test

foo "bar

Centered text.

Again.

AOU a simple test of a table
German Umlauts
ÄÖÜ
äöü

Lorem ipsum dolor sit amet, consectetur adipisici elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

© someone

EOD readln File.open($workdir+'temp.html', 'w') do |f| f.write data end puts '-- -- -- -- -- -- --' print `#{@conf[nr]['htmlcmd']} #{$workdir}temp.html` puts '-- -- -- -- -- -- --' readln end def show(nr,v) if @conf[nr][v].nil? if @conf[0][v].nil? '[not set]' elsif nr.zero? @conf[0][v].to_s else "#{@conf[0][v]} [global]" end else @conf[nr][v].to_s end end def testmeth(nr) if not @conf[nr]['test'].nil? @conf[nr]['test'] elsif not @conf[0]['test'].nil? @conf[0]['test'] else DefaultTest end end def configure_urlmenu(nr) loop do puts "\n\n" if nr == 0 then possible=[] puts " [ ]\tGlobal" else possible=['u'] puts " [u]\tURI: #{show(nr,'url')}" end puts " [m]\tTest method: #{testmeth nr}" puts " [x]\tMenu title: #{show(nr,'title')}" possible += ['m','x'] puts if show(nr,'url')=~ %r(^ftp://)i puts " [t]\tTimeout: #{show(nr,'timeout')}" puts " [v]\tPassive server: #{show(nr,'passive')}" possible += ['t', 'v'] else # HTTP puts " [a]\tUser-Agent: #{show(nr,'client')}" puts " [r]\tReferer: #{show(nr,'referer')}" puts " [l]\tAccept-Language: #{show(nr,'lang')}" puts " [b]\tRange: #{show(nr,'range')}" puts " [t]\tTimeout: #{show(nr,'timeout')}" puts " [p]\tProxy: #{show(nr,'proxy')}" possible += ['a', 'r', 'l', 'b', 't', 'p'] if testmeth(nr) == 'html' puts " [h]\tCommand HTML dumper: #{show(nr,'htmlcmd')}" possible << 'h' else puts " [ ]\tCommand HTML dumper: [only available w/ html]" end if nr != 0 if ['html','diff'].member?(testmeth(nr)) puts " [s]\tStart regexp: #{show(nr,'start')}" puts " [e]\tEnd regexp: #{show(nr,'end')}" possible += ['s', 'e'] else puts " [ ]\tStart regexp: [only avail w/ diff or html]" puts " [ ]\tEnd regexp: [only avail w/ diff or html]" end end end puts if nr > 0 puts " [d]\tDelete URI from list." possible << 'd' end puts " [f]\tI have finished." possible << 'f' case (choice = choose(possible)) when 'a' then configure_useragent(nr) when 'b' then configure_range(nr) when 'd' @conf.delete_at(nr) return when 'e' then @conf[nr]['end'] = ask_regexp when 'f' then return when 'h' then configure_htmlcmd(nr) when 'l' then configure_lang(nr) when 'm' then configure_testmethod(nr) when 'p' then configure_proxy(nr) when 'r' then configure_referer(nr) when 's' then @conf[nr]['start'] = ask_regexp when 't' then configure_timeout(nr) when 'u' then configure_url(nr) when 'v' then configure_passive(nr) when 'x' then configure_title(nr) end end end end class Main def initialize @config = Config.new @store = Store.new cleanup end def cleanup @store.each do |key| verify = false @config.each do |e| verify = true if key == name(e['test'], e['url'], e['range']) end unless verify @store.delete(key) $log.puts "deleted stored `#{key}' data" end end end end # the following module code has been taken from: # libalgorithm-diff-ruby version 0.4 # and has been changed. # # Original copyright notice: # # algorithm/diff - a Ruby module to compute difference sets between two # # objects. Copyright (c) 2001-2002 Lars Christensen. # # # # This program is free software; you can redistribute it and/or modify # # it under the terms of the GNU General Public License as published by # # the Free Software Foundation; either version 2 of the License, or # # (at your option) any later version. # # # # This program is distributed in the hope that it will be useful, but # # WITHOUT ANY WARRANTY; without even the implied warranty of # # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. module Diff attr_reader :diffs # Change notes: # The mix-in module Diffable has been removed. # The functions reverse_hash and replacenextlarger have been put here. # The lcs function has just been copied, reindented and reverse_hash / # replacenextlarger changes have been applied. # makediff has been altered to yield both indices and no string. (This is not # really needed, but more `natural' in my opinion.) # diff has been rewritten to produce a imho `nicer' output. # groupwise and unified have been completely written by me (sbeyer) and are # not part of the original libalgorithm-diff-ruby package. # Create a hash that maps elements of the array to arrays of indices # where the elements are found. def Diff.reverse_hash(obj, range = (0...obj.length)) revmap = {} range.each do |i| elem = obj[i] if revmap.has_key? elem revmap[elem].push i else revmap[elem] = [i] end end revmap end # Replace the first element of obj which is larger than value. Assumes that # the element indexed by high, if given is larger than value. def Diff.replacenextlarger(obj, value, high = nil) high ||= obj.length low = 0 index = found = nil while low < high index = (high+low) >> 1 found = obj[index] if value > found # this first, most common case low = index + 1 elsif value == found return nil else high = index end end obj[low] = value low end def Diff.lcs(a, b) astart = 0 bstart = 0 afinish = a.length-1 bfinish = b.length-1 mvector = [] # First we prune off any common elements at the beginning while (astart <= afinish && bstart <= afinish && a[astart] == b[bstart]) mvector[astart] = bstart astart += 1 bstart += 1 end # now the end while (astart <= afinish && bstart <= bfinish && a[afinish] == b[bfinish]) mvector[afinish] = bfinish afinish -= 1 bfinish -= 1 end bmatches = reverse_hash(b, bstart..bfinish) thresh = [] links = [] (astart..afinish).each do |aindex| aelem = a[aindex] next unless bmatches.has_key? aelem k = nil bmatches[aelem].reverse_each do |bindex| if k && (thresh[k] > bindex) && (thresh[k-1] < bindex) thresh[k] = bindex else k = replacenextlarger(thresh, bindex, k) end links[k] = [ k!=0 && links[k-1], aindex, bindex ] if k end end if !thresh.empty? link = links[thresh.length-1] while link mvector[link[1]] = link[2] link = link[0] end end return mvector end def Diff.makediff(a, b) mvector = Diff.lcs(a, b) ai = bi = 0 while ai < mvector.length bline = mvector[ai] if bline while bi < bline yield :+, ai, bi bi += 1 end bi += 1 else yield :-, ai, bi end ai += 1 end while ai < a.length yield :-, ai, bi ai += 1 end while bi < b.length yield :+, ai, bi bi += 1 end 1 end def Diff.diff(a, b, &block) diffs = [[:info, a.length, b.length]] # dirty trick block ||= proc do |action, li, ri| diffs << [action, li, ri] end Diff.makediff(a, b, &block) diffs end # group diff by context def Diff.groupwise(diff, context) (_,alen,blen) = diff.shift # dirty trick to get length information groups = [] # array of groups unless diff.empty? g = [] # one group low = 0 (el0,el1,el2) = diff.shift begin [low, el1-context].max.upto(el1-1) do |i| g.push [:eq, i, i+el2-el1] end begin g.push [el0,el1,el2] low = el1 low += 1 if el0 == :- if diff.empty? el1 = blen low.upto([low+context,alen].min-1) do |i| g.push [:eq, i, i+blen-alen] end break else (el0,el1,el2) = diff.shift low.upto([el1,low+context].min-1) do |i| g.push [:eq, i, i+el2-el1] end end end while el1 - low <= context if el1 - low <= 2*context and el1 != blen low += 2 # didn't figure out why, but this # worked on all examples! next end groups.push g yield g g = [] end until diff.empty? end groups end def Diff.unified(la,ra,context=3) groupwise(diff(la,ra),context) do |g| # count :- and :+ minus = g.find_all do |z| z[0]==:- end.length plus = g.find_all do |z| z[0]==:+ end.length equal = g.length - minus - plus minus += equal plus += equal str = '@@ -' if minus > 1 str << "#{g[0][1]+1},#{minus}" elsif minus == 1 str << (g[0][1]+1).to_s else # minus == 0 str << "#{g[0][1]},0" end str << ' +' if plus > 1 str << "#{g[0][2]+1},#{plus}" elsif plus == 1 str << (g[0][2]+1).to_s else # plus == 0 str << "#{g[0][2]},0" end str << " @@\n" yield str g.each do |x| if x[0] == :eq then str = ' '+la[x[1]] elsif x[0] == :- then str = '-'+la[x[1]] else str = '+'+ra[x[2]] end yield str end end end end module XXX # how to name that? ;-) # simple regular <.*> matcher and remover. # Could fail on nested , on wrong HTML and on Javascript. def XXX.tagstrip(str) str.gsub(/([\r\n]|<[^<>]*>)+/,"\n").gsub(/[ \t]+/,' ').gsub(/([\r\n][ \t]+)+[\r\n]?/, "\n") end # cuts out string between first match to "starts" (a regexp string or # nil) and last match to "ends" (regexp string or nil) def XXX.cutout(starts, ends, str) unless starts.nil? begin match = str.match(str_to_re(starts)) rescue SyntaxError errorout "Error on parsing start regex \"#{starts}\"." errorout $! raise end match = str.match(str_to_re(starts)) str = str[match.begin(0)..-1] unless match.nil? end unless ends.nil? pos = 0 begin begin match = str[pos..-1].match(str_to_re(ends)) rescue SyntaxError errorout "Error on parsing end regex \"#{ends}\"." errorout $! raise end match = str[pos..-1].match(str_to_re(ends)) pos += match.end(0) unless match.nil? end until match.nil? str = str[0..pos-1] end str += "\n" unless [?\n,?\r].member?(str[-1]) str end def XXX.process_html(data,e) s = e['htmlcmd'] if s.nil? tagstrip(data) else File.open($workdir+'temp.html', 'w') do |f| f.write data end `#{s} #{$workdir}temp.html` # or replace %s? We don't need it, eh? end end end class MainDiff < Main def initialize super @config.each do |e| method = e['test'] next unless ['diff','html'].member?(method) ## will we change this and generate pseudo-diffs? key = name(method, e['url'], e['range']) list = @store[key] next if list.nil? if e['url'] =~ %r(^ftp://)i # do nothing special next if list[0] == list[1] else # HTTP specialities list[0] = XXX.cutout(e['start'],e['end'], list[0]) list[1] = XXX.cutout(e['start'],e['end'], list[1]) next if list[0] == list[1] list[0] = XXX.process_html(list[0],e) if method == 'html' list[1] = XXX.process_html(list[1],e) if method == 'html' next if list[0] == list[1] end puts '--- '+e['url'] puts '+++ '+e['url'] file1, file2 = [], [] list[0].each_line do |line| file1 << line end list[1].each_line do |line| file2 << line end Diff.unified(file1,file2,DefaultContext) do |str| print str end end end end class MainReset < Main def initialize super @config.each do |e| method = e['test'] next unless ['diff','html'].member?(method) key = name(method, e['url'], e['range']) list = @store[key] next if list.nil? next if list[0] == list[1] list[0] = list[1] @store[key] = list end end end class MainGet < Main require 'net/http' require 'uri' def initialize super fetchurls end def updatedata(key, value) if value.nil? errorout "#{key} - not set! choose another test method!" return end unless @store[key].nil? or value == @store[key] yield $log.puts "#{key} - changed" end @store[key] = value end def diffdata(key, value) if @store[key].nil? @store[key] = [value, value] $log.puts "#{key} - set" else if @store[key][1] != value @store[key] = [@store[key][1], value] $log.puts "#{key} - changed" if yield @store[key][0], @store[key][1] end end end def httpget(e, url, doit) header = {} header['User-Agent'] = e['client'] unless e['client'].nil? header['Referer'] = e['referer'] unless e['referer'].nil? header['Accept-Language'] = e['lang'] unless e['lang'].nil? header['Range'] = 'bytes=' + e['range'] unless e['range'].nil? header['Authorization'] ='Basic '+[url.userinfo].pack('m') unless url.userinfo.to_s.empty? header['Connection'] = 'close' # default header['Cache-Control'] = 'no-cache' # Thanks to Alexander Logvinov! header['Pragma'] = 'no-cache' # this, too proxy, proxyport = e['proxy'].split(/:/) unless e['proxy'].nil? proxyport = proxyport.to_i unless proxyport.nil? timeout = e['timeout'] method = e['test'] path = '/' path = url.path unless url.path.to_s.empty? path << '?'+url.query unless url.query.to_s.empty? conn = Net::HTTP.new(url.host, url.port, proxy, proxyport) # you can add "proxyuser", "proxypass" strings for proxy auth conn.open_timeout = timeout.to_i conn.start do resp = {} if ['size','md5sum','date'].member?(method) resp = conn.head(path, header) # only need header (HEAD request) case method when 'size' updatedata(name(method,e['url']), resp['Content-Length'], &doit) when 'md5sum' updatedata(name(method,e['url']), resp['Content-MD5'], &doit) when 'date' updatedata(name(method,e['url']), resp['Last-Modified'], &doit) end else resp = conn.get(path, header) # get body (GET request) if method == 'diff' or method == 'html' begin diffdata(name(method, e['url'], e['range']), resp.body) do |old,new| old = XXX.cutout(e['start'],e['end'], old) new = XXX.cutout(e['start'],e['end'], new) if method == 'html' old = XXX.process_html(old,e) new = XXX.process_html(new,e) end if old != new doit.call true else false end end rescue SyntaxError $errors += 1 end else # a regexp begin if resp.body =~ str_to_re(method) doit.call end rescue SyntaxError errorout "Error on parsing regex \"#{method}\"." errorout $! $errors += 1 end end end end end def ftpget(e, url, doit) Net::FTP.open(url.host, url.port, url.user, url.password) do |ftp| url.path << '/' if url.path.nil? or url.path.empty? case e['test'] when 'date', 'size' begin if e['test'] == 'date' s = ftp.mdtm(url.path) else s = ftp.size(url.path) end updatedata(name(e['test'], e['url']), s, &doit) rescue Net::FTPPermError case $!.message[0,3] when '530' ftp.login('anonymous',ProgName) retry else #p $!.class #p $!.message raise end end when 'diff' ftp.passive = true unless e['passive'].downcase == 'false' if url.path[-1] == ?/ begin s = ftp.listing(url.path) rescue Net::FTPPermError case $!.message[0,3] when '530' ftp.login('anonymous',ProgName) retry else #p $!.class #p $!.message raise end end else # assume a (binary) file (RETR). Failing? try LIST ;-) begin s = ftp.retr(url.path) rescue Net::FTPPermError case $!.message[0,3] when '530' ftp.login('anonymous',ProgName) retry when '550' s = ftp.listing(url.path) else #p $!.class #p $!.message raise end end # is it better to CWD, LIST, check if d-prefixed, then LIST else RETR? # I think the current variant is better ;-) end ftp.quit diffdata(name('diff', e['url']), s) do |old,new| if old != new doit.call true else false end end end end end def fetchurls @config.each do |e| begin url = URI.parse(e['url']) doit = proc { puts url } scm = url.scheme.downcase if scm[0,4] == 'http' httpget(e, url, doit) elsif scm == 'ftp' ftpget(e, url, doit) end rescue SocketError, Timeout::Error, URI::InvalidURIError, EOFError, Errno::ECONNREFUSED, Net::ProtocolError, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::FTPError # should we just rescue StandardError? errorout e['url']+': '+$! $errors += 1 end end end end class Configure def initialize @config = Config.new false @config.configure end end ### This is int main(...) ;-) command = getcmd(Options.init) # Options.init is a function with side effects Dir.mkdir($workdir) unless File.exist?($workdir) $log = File.new($workdir+'lastlog', 'w') END { $log.close } # commands case command when 'configure' Configure.new when 'diff' MainDiff.new when 'help' showversion showusage when 'reset' MainReset.new when 'version' showversion else MainGet.new #begin # MainGet.new #rescue StandardError # errorout $! # $errors += 1 #end ### this is nicer, but ugly for bug reporting! end exit $errors