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 ? " " : " "
" "
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` | / \ < > v ^ - _ ( ) `
+
+ chars.each_with_index do |char, idx|
+ content =
+ case char
+ when %r/\n/o
+ "<br>"
+ when %r/\s/o
+ #rand > 0.42 ? " " : " "
+ " "
+ 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