lib/flatulent.rb in flatulent-0.0.1 vs lib/flatulent.rb in flatulent-0.0.2

- old
+ new

@@ -1,98 +1,285 @@ -require 'digest/md5' +# TODO - encrypt time +# TODO - noise is image chars +# TODO - newline in value?? +# TODO - vertical offset in chars -libdir = File.expand_path(__FILE__).gsub(%r/\.rb$/, '') + File::SEPARATOR +class Flatulent + Flatulent::VERSION = '0.0.2' unless defined? Flatulent::VERSION + def self.flatulent() Flatulent::VERSION end + def self.libdir() File.expand_path(__FILE__).gsub(%r/\.rb$/, '') end -begin - require 'rubygems' -rescue LoadError -end + require 'cgi' + require 'base64' + require 'digest/md5' -begin - require 'text/figlet' -rescue LoadError - require libdir + 'text/figlet' -end + begin + require 'rubygems' + rescue LoadError + end -begin - require 'pervasives' -rescue LoadError - require libdir + 'pervasives' -end + begin + $:.unshift libdir + require 'text/figlet' + require 'crypt/blowfish' + require 'pervasives' + require 'attributes' + ensure + $:.shift + end -begin - require 'attributes' -rescue LoadError - require libdir + 'attributes' -end + class Error < ::StandardError; end + class EncryptionError < Error; end + class TimeBombError < Error; end -class Flatulent + singleton_class = class << self self end singleton_class.module_eval do - attribute('libdir'){ File.expand_path(__FILE__).gsub(%r/\.rb$/, '') } attribute('fontdir'){ File.join libdir, 'fontfiles' } attribute('style'){ Hash[ - 'white-space' => 'pre,nowrap', + 'white-space' => 'pre', 'font-family' => 'monospace', 'font-weight' => 'bold', - 'display' => 'table', - 'padding' => '11px', + 'font-size' => 'medium', 'background' => '#ffc', - 'color' => 'blue', + 'color' => '#00f', + 'margin' => '2px', + 'padding' => '2px', + 'display' => 'table', ] } attribute('noise_style'){ Hash[ 'color' => '#ccc', - 'font-style' => 'oblique', ] } + + attribute('key'){ default_key } + + def valid? keywords = {} + begin + validate! keywords + true + rescue EncryptionError, TimeBombError + false + end + end + + def validate! keywords = {} + keywords = keywords['flatulent'] if keywords.has_key?('flatulent') + keywords = keywords[:flatulent] if keywords.has_key?(:flatulent) + + opts = getopts keywords + + captcha = opts['c'] or raise 'no captcha' + string = opts['s'] or raise 'no string' + time = opts['t'] or raise 'no time' + + expected = zeroh(decrypt(string)) + actual = zeroh(captcha) + raise EncryptionError, "expected #{ expected } got #{ actual }" unless + expected == actual + + timebomb = Time.at(Integer(decrypt(time))).utc rescue(raise("bad time #{ time }")) + raise TimeBombError unless Time.now.utc <= timebomb + + return actual + end + + def zeroh string + string.gsub(%r/[0Oo]/, '0') # ignore diffs between 0/o/O + end + + def blowfish + @blowfish ||= Hash.new{|h,k| h[k] = Crypt::Blowfish.new(key)} + end + + def munge string + string.strip.downcase + end + + def encrypt string + Base64.encode64(blowfish[key].encrypt_string(string.to_s)).chop # kill "\n" + end + + def decrypt string + munge(blowfish[key].decrypt_string(Base64.decode64("#{ string }\n"))) + end + + def getopts options + lambda do |key, *default| + default = default.first + break options[key] if options.has_key?(key) + key = key.to_s + break options[key] if options.has_key?(key) + key = key.to_sym + break options[key] if options.has_key?(key) + break default + end + end + + def default_key + #attribute('default_key') do + return @default_key if defined? @default_key + require 'socket' + hostname = Socket.gethostname + maddr = mac_address rescue nil + warn "could not determine mac addresss!" unless maddr + #puts(( Digest::MD5.hexdigest "--#{ hostname }--#{ maddr }--" )) + #Digest::MD5.hexdigest "--#{ hostname }--#{ maddr }--" + #@default_key = "--#{ hostname }--#{ maddr }--" + @default_key = "--#{ hostname }--#{ maddr }--" + end + + def mac_address + return @mac_address if defined? @mac_address + re = %r/[^:\-](?:[0-9A-F][0-9A-F][:\-]){5}[0-9A-F][0-9A-F][^:\-]/io + cmds = '/sbin/ifconfig', '/bin/ifconfig', 'ifconfig', 'ipconfig /all' + + null = test(?e, '/dev/null') ? '/dev/null' : 'NUL' + + lines = nil + cmds.each do |cmd| + stdout = IO.popen("#{ cmd } 2> #{ null }"){|fd| fd.readlines} rescue next + next unless stdout and stdout.size > 0 + lines = stdout and break + end + raise "all of #{ cmds.join ' ' } failed" unless lines + + candidates = lines.select{|line| line =~ re} + raise 'no mac address candidates' unless candidates.first + candidates.map!{|c| c[re]} + + maddr = candidates.first + raise 'no mac address found' unless maddr + + maddr.strip! + maddr.instance_eval{ @list = candidates; def list() @list end } + + @mac_address = maddr + end + + def figlet options = {} + new(options).figlet + end + def figlets options = {} + new(options).figlets + end + def element options = {} + new(options).element + end + def form_tags options = {} + new(options).form_tags + end + def form options = {} + new(options).form + end + + def latest_prototype_lib + 'http://www.prototypejs.org/assets/2007/6/20/prototype.js' + end + def require_prototype + %Q` + <script type="text/javascript"> + var prototype = true; + try{ Prototype; } catch(e) { prototype = false }; + if(!prototype){ + var js = document.createElement('script'); + js.type='text/javascript'; + js.src='/javascripts/prototype.js'; + document.getElementsByTagName('head')[0].appendChild(js); + } + </script> + <script type="text/javascript"> + var prototype = true; + try{ Prototype; } catch(e) { prototype = false }; + if(!prototype){ + var js = document.createElement('script'); + js.type='text/javascript'; + js.src='#{ latest_prototype_lib }'; + document.getElementsByTagName('head')[0].appendChild(js); + } + </script> + ` + end + def javascript options = {} + id = options[:id] || options['id'] || 'flatulent' + url = options[:url] || options['url'] || '/flatulent/captcha' + %Q` + #{ require_prototype } + <script type="text/javascript"> + new Ajax.Updater('#{ id }', '#{ url }', { method: 'get' }); + </script> + ` + end + def ajax options = {} + id = options[:id] || options['id'] || 'flatulent' + %Q` + <div id="#{ id }"></div> + #{ Flatulent.javascript } + ` + end end singleton_class.attributes.each{|a| attribute(a){ self.class.send a}} - attr 'string' + attribute 'string' + attribute 'size' + attribute 'font' + attribute 'noise' + attribute 'id' + attribute 'action' + attribute 'ttl' - def initialize keywords = {} - opt = getopts keywords + attribute 'figlet' + attribute 'figlets' + attribute 'element' + attribute 'form_tags' + attribute 'form' - @size = opt[ 'size', 4 ] - @string = opt[ 'string', generate_random_string ] - @font = opt[ 'font', 'big' ] - @noise = opt[ 'noise', 0.04 ] - @no_table = opt[ 'no_table', false ] - @id = opt[ 'id', 'flatulent' ] - @action = opt[ 'action' ] - @ttl = Integer opt[ 'ttl', 5 * 60 ] # seconds! + def initialize arg = {} + if Hash === arg + opt = getopts arg + @size = Integer opt[ 'size', 4 ] + @string = String opt[ 'string', generate_random_string ] + else + opt = getopts Hash.new + @string = String arg + @size = @string.size + end + + @font = String opt[ 'font', 'big' ] + @noise = Float opt[ 'noise', 0.04 ] + @id = String opt[ 'id', 'flatulent' ] + @action = String opt[ 'action' ] + @ttl = Integer opt[ 'ttl', 256 ] figlet! element! form_tags! form! end def generate_random_string chars = ('A' .. 'Z').to_a + ('1' .. '9').to_a ### zero is too much like o/O - Array.new(@size).map{ chars[rand(chars.size - 1)]}.join #.join(' ') + Array.new(@size).map{ chars[rand(chars.size - 1)]}.join end - def getopts options - self.class.getopts options - end - def figlet! - spaced = @string.split(%r//).join(' ') + spaced = @string.split(%r//).join #.join(' ') fontfile = File.join fontdir, "#{ @font }.flf" font = Text::Figlet::Font.new fontfile typesetter = Text::Figlet::Typesetter.new font + @figlets = [] + chars = spaced.split %r// + chars.each{|char| @figlets << typesetter[char]} @figlet = typesetter[spaced] end - attr 'figlet' def element! rows = [] rows << (row = []) chars = @figlet.split %r// @@ -103,137 +290,161 @@ content = case char when %r/\n/o "<br>" when %r/\s/o + #rand > 0.42 ? "&nbsp;" : " " "&nbsp;" when %r/([^\s])/o - $1 + CGI.escapeHTML $1 end + Array.new(rand(42)){ content = "<span>#{ content }</span>"} row << content rows << (row = []) unless idx == last end + noisy = %W` | / - _ ( ) \ ` (@noise * chars.size).ceil.times do y = rand(rows.size - 1) x = rand(rows.first.size - 1) - next if rows[y][x] == '<br>' - printable = rand(126 - 33) + 33 + 1 - rows[y][x] = "<span 'style=#{ noise_css }'>#{ printable.chr }</span>" + next if rows[y][x] =~ %r"br" + char = noisy[ rand(noisy.size) ] + rows[y][x] = char end content = rows.join @element = "<pre id='#{ @id }_element' style='#{ css }'>#{ content }</pre>" end - attr 'element' +=begin + + def element! + cells = [] + + @figlets.each do |figlet| + rows = [] + rows << (row = []) + + offset_t = Array.new(rand(4)).map{ "\n"} + offset_b = Array.new(rand(4)).map{ "\n"} + + offset_l = Array.new(rand(4)).map{ " "} + offset_r = Array.new(rand(4)).map{ " "} + + chars = offset_t + figlet.split(%r//) + offset_b + size = chars.size + last = size - 1 + #drawn = chars.select{|char| char !~ %r/\s/} + drawn = %w` | / \ &lt; &gt; v ^ - _ ( ) ` + + chars.each_with_index do |char, idx| + content = + case char + when %r/\n/o + "<br>" + when %r/\s/o + #rand > 0.42 ? "&nbsp;" : " " + "&nbsp;" + when %r/([^\s])/o + CGI.escapeHTML $1 + end + #rand(10).times{ content = "<span>#{ content }</span>" } + row << content + rows << (row = []) unless idx == last + end + + noisy = %w` | / \ - _ ( ) ` + (@noise * chars.size).ceil.times do + y = rand(rows.size - 1) + x = rand(rows.first.size - 1) + next if rows[y][x] == "<br>" + char = noisy[ rand(noisy.size) ] + rows[y][x] = char + end + + content = rows.join + cells << content + end + + formatted = lambda{|x| "<pre class='#{ @id }_figlet' style='#{ css }'>#{ x }</pre>"} + + @element = + "<table id='#{ @id }_element' border='0' cellpadding='0' cellspacing='0' bgcolor='#{ style["background"] }'><tr>" << + cells.map{|cell| "<td>#{ formatted[cell] }</td>"}.join << + "</tr></table>" + end +=end + def css - style.map{|kv| kv.join ':'}.join ';' + css_for style end def noise_css - noise_style.map{|kv| kv.join ':'}.join ';' + css_for noise_style end + def css_for hash + hash.map{|kv| kv.join ':' }.join ';' + end + def form_tags! n = @string.scan(%r/\w/).size + string = @string + timebomb = Time.now.utc.to_i + @ttl @form_tags = <<-html #{ element } <p id='#{ @id }_instructions'> - Please enter the #{ n } large characters shown. + Please enter the #{ n } large characters (A-Z, 1-9) shown. </p> - <input type='textarea' name='#{ @id }[t]' id='#{ @id }_textarea' /> - <input type='hidden' name='#{ @id }[m]' id='#{ @id }_m' value='#{ md5 }' /> - <input type='hidden' name='#{ @id }[v]' id='#{ @id }_v' value='#{ valid_until }' /> + <input type='textarea' name='#{ @id }[c]' id='#{ @id }_textarea' /> + <input type='hidden' name='#{ @id }[s]' id='#{ @id }_e' value='#{ encrypt string }' /> + <input type='hidden' name='#{ @id }[t]' id='#{ @id }_v' value='#{ encrypt timebomb }' /> html end - attr 'form_tags' + alias_method 'to_html', 'form_tags' - def md5 - Digest::MD5.hexdigest munge(@string) + def encrypt string + self.class.encrypt string.to_s end + def encrypted + self.class.encrypt @string + end + def munge string self.class.munge string end - def self.munge string - string.downcase.strip.gsub(%r/[0Oo]/, '0') ### note that 0 (zero) looks like O and o - end - def valid_until - Time.now.to_i + @ttl - end - def form! action = "action='#{ @action }'" @form = <<-html <form method='post' #{ action }> #{ form_tags } <input type='submit' name='#{ @id }[submit]' id='#{ @id }_submit' value='Submit' /> </form> html end - attr 'form' - alias_method 'to_html', 'form' def to_html - @element + element end def to_s form end - class MD5Error < ::StandardError; end - class TimeToLiveError < ::StandardError; end - TTLError = TimeToLiveError - - def self.valid? keywords = {} - begin - validate! keywords - true - rescue MD5Error, TTLError - false - end + def getopts options + self.class.getopts options end +end - def self.validate! keywords = {} - keywords = keywords['flatulent'] if keywords.has_key?('flatulent') - keywords = keywords[:flatulent] if keywords.has_key?(:flatulent) +def Flatulent(*a, &b) Flatulent.new(*a, &b) end - opts = getopts keywords +if $0 == __FILE__ + #string = rand.to_s + #puts Flatulent(string) + #Flatulent.validate! :t => string, :e => Flatulent.encrypt(string), :v => (Time.now.utc.to_i + 60) + #e = Flatulent.encrypt('foobar') + #p e + #p Flatulent.decrypt(e) - textarea = opts['t'] - md5 = opts['m'] - timestamp = Integer opts['v'] - - raise MD5Error unless Digest::MD5.hexdigest(munge(textarea)) == md5 - raise TimeToLiveError unless Time.now.utc <= Time.at(timestamp).utc - true - end - - def self.getopts options - lambda do |key, *default| - default = default.first - break options[key] if options.has_key?(key) - key = key.to_s - break options[key] if options.has_key?(key) - key = key.to_sym - break options[key] if options.has_key?(key) - break default - end - end - - def self.form options = {} - new(options).form - end - def self.element options = {} - new(options).element - end - def self.form_tags options = {} - new(options).form_tags - end - def self.figlet options = {} - new(options).figlet - end + puts Flatulent.figlet('foobar') end - -def Flatulent(*a, &b) Flatulent.new(*a, &b) end