# TODO - encrypt time # TODO - noise is image chars # TODO - newline in value?? # TODO - vertical offset in chars 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 require 'cgi' require 'base64' require 'digest/md5' begin require 'rubygems' rescue LoadError end begin $:.unshift libdir require 'text/figlet' require 'crypt/blowfish' require 'pervasives' require 'attributes' ensure $:.shift end class Error < ::StandardError; end class EncryptionError < Error; end class TimeBombError < Error; end singleton_class = class << self self end singleton_class.module_eval do attribute('fontdir'){ File.join libdir, 'fontfiles' } attribute('style'){ Hash[ 'white-space' => 'pre', 'font-family' => 'monospace', 'font-weight' => 'bold', 'font-size' => 'medium', 'background' => '#ffc', 'color' => '#00f', 'margin' => '2px', 'padding' => '2px', 'display' => 'table', ] } attribute('noise_style'){ Hash[ 'color' => '#ccc', ] } 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` ` end def javascript options = {} id = options[:id] || options['id'] || 'flatulent' url = options[:url] || options['url'] || '/flatulent/captcha' %Q` #{ require_prototype } ` end def ajax options = {} id = options[:id] || options['id'] || 'flatulent' %Q`
#{ Flatulent.javascript } ` end end singleton_class.attributes.each{|a| attribute(a){ self.class.send a}} attribute 'string' attribute 'size' attribute 'font' attribute 'noise' attribute 'id' attribute 'action' attribute 'ttl' attribute 'figlet' attribute 'figlets' attribute 'element' attribute 'form_tags' attribute 'form' 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.03 ] @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 end def figlet! 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 def element! rows = [] rows << (row = []) chars = @figlet.split %r// size = chars.size last = size - 1 chars.each_with_index do |char, idx| content = case char when %r/\n/o "
" when %r/\s/o #rand > 0.42 ? " " : " " " " when %r/([^\s])/o CGI.escapeHTML $1 end Array.new(rand(42)){ content = "#{ content }"} 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] =~ %r"
" char = noisy[ rand(noisy.size) ] rows[y][x] = char end content = rows.join @element = "
#{ content }
" end =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` | / \ < > v ^ - _ ( ) ` chars.each_with_index do |char, idx| content = case char when %r/\n/o "
" when %r/\s/o #rand > 0.42 ? " " : " " " " when %r/([^\s])/o CGI.escapeHTML $1 end #rand(10).times{ content = "#{ content }" } 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] == "
" char = noisy[ rand(noisy.size) ] rows[y][x] = char end content = rows.join cells << content end formatted = lambda{|x| "
#{ x }
"} @element = "" << cells.map{|cell| ""}.join << "
#{ formatted[cell] }
" end =end def css css_for style end def noise_css 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 }

Please enter the #{ n } large characters (A-Z, 1-9) shown.

html end alias_method 'to_html', 'form_tags' 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 form! action = "action='#{ @action }'" @form = <<-html
#{ form_tags }
html end def to_html element end def to_s form end def getopts options self.class.getopts options end end def Flatulent(*a, &b) Flatulent.new(*a, &b) end 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) puts Flatulent.figlet('foobar') end