deathbycaptcha 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +88 -0
- data/Rakefile +2 -0
- data/deathbycaptcha.gemspec +24 -0
- data/lib/deathbycaptcha.rb +9 -0
- data/lib/deathbycaptcha/client.rb +197 -0
- data/lib/deathbycaptcha/config.rb +42 -0
- data/lib/deathbycaptcha/error.rb +62 -0
- data/lib/deathbycaptcha/http_client.rb +93 -0
- data/lib/deathbycaptcha/socket_client.rb +249 -0
- data/lib/deathbycaptcha/version.rb +4 -0
- metadata +106 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (C) 2011 by Infosimples. http://www.infosimples.com.br/
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
== DeathByCaptcha
|
2
|
+
|
3
|
+
DeathByCaptcha is a Ruby API for acessing the http://www.deathbycaptcha.com services.
|
4
|
+
|
5
|
+
It supports HTTP and socket-based connections, with the latter being recommended for having faster responses and overall better performance.
|
6
|
+
|
7
|
+
When using socket connections, make sure that outgoing TCP traffic to <b>api.deathbycaptcha.com</b> to the ports range in <b>8123-8130</b> is not blocked by your firewall.
|
8
|
+
|
9
|
+
=== Thread-safety note
|
10
|
+
|
11
|
+
The API is thread-safe, which means it is perfectly fine to share a client instance between multiple threads.
|
12
|
+
|
13
|
+
=== Installation
|
14
|
+
|
15
|
+
You can install the latest DeathByCaptcha gem with:
|
16
|
+
|
17
|
+
gem install deathbycaptcha
|
18
|
+
|
19
|
+
You can add it to your Gemfile:
|
20
|
+
|
21
|
+
gem 'deathbycaptcha'
|
22
|
+
|
23
|
+
=== Examples
|
24
|
+
|
25
|
+
==== Create a client
|
26
|
+
|
27
|
+
===== HTTP client
|
28
|
+
|
29
|
+
require 'deathbycaptcha'
|
30
|
+
|
31
|
+
client = DeathByCaptcha.http_client('myusername', 'mypassword')
|
32
|
+
|
33
|
+
===== Socket client
|
34
|
+
|
35
|
+
require 'deathbycaptcha'
|
36
|
+
|
37
|
+
client = DeathByCaptcha.socket_client('myusername', 'mypassword')
|
38
|
+
|
39
|
+
==== Verbose mode (for debugging purposes)
|
40
|
+
|
41
|
+
client.is_verbose = true
|
42
|
+
|
43
|
+
==== Decoding captcha
|
44
|
+
|
45
|
+
===== From URL
|
46
|
+
|
47
|
+
response = client.decode 'http://www.phpcaptcha.org/securimage/securimage_show.php'
|
48
|
+
|
49
|
+
puts "captcha id: #{response['captcha']}, solution: #{response['text']}, is_correct: #{response['is_correct']}}"
|
50
|
+
|
51
|
+
===== From file path
|
52
|
+
|
53
|
+
response = client.decode 'path/to/my/captcha/file'
|
54
|
+
|
55
|
+
puts "captcha id: #{response['captcha']}, solution: #{response['text']}, is_correct: #{response['is_correct']}}"
|
56
|
+
|
57
|
+
===== From a file
|
58
|
+
|
59
|
+
file = File.open('path/to/my/captcha/file', 'r')
|
60
|
+
|
61
|
+
response = client.decode file
|
62
|
+
|
63
|
+
puts "captcha id: #{response['captcha']}, solution: #{response['text']}, is_correct: #{response['is_correct']}}"
|
64
|
+
|
65
|
+
===== From raw content
|
66
|
+
|
67
|
+
raw_content = File.open('path/to/my/captcha/file', 'r').read
|
68
|
+
|
69
|
+
response = client.decode(raw_content, :raw_content => true)
|
70
|
+
|
71
|
+
puts "captcha id: #{response['captcha']}, solution: #{response['text']}, is_correct: #{response['is_correct']}}"
|
72
|
+
|
73
|
+
==== Get the solution of a captcha
|
74
|
+
|
75
|
+
puts client.get_captcha('130920620')['text'] # where 130920620 is the captcha id
|
76
|
+
|
77
|
+
==== Get user account information
|
78
|
+
|
79
|
+
puts client.get_user
|
80
|
+
|
81
|
+
=== Maintainers
|
82
|
+
|
83
|
+
* Rafael Barbolo Lopes (http://github.com/barbolo)
|
84
|
+
* Rafael Ivan Garcia (http://github.com/rafaelivan)
|
85
|
+
|
86
|
+
== License
|
87
|
+
|
88
|
+
MIT License. Copyright (C) 2011 by Infosimples. http://www.infosimples.com.br
|
data/Rakefile
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "deathbycaptcha/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.add_dependency 'rest-client', '~> 1.6.1'
|
7
|
+
s.add_dependency 'json', '~> 1.4.6'
|
8
|
+
|
9
|
+
s.name = "deathbycaptcha"
|
10
|
+
s.version = DeathByCaptcha::VERSION
|
11
|
+
s.platform = Gem::Platform::RUBY
|
12
|
+
s.authors = ["Rafael Barbolo Lopes, Rafael Ivan Garcia"]
|
13
|
+
s.email = ["tech@infosimples.com.br"]
|
14
|
+
s.homepage = "http://github.com/infosimples/deathbycaptcha"
|
15
|
+
s.summary = %q{Ruby API for DeathByCaptcha (Captcha Solver as a Service)}
|
16
|
+
s.description = %q{Ruby API for DeathByCaptcha (Captcha Solver as a Service)}
|
17
|
+
|
18
|
+
s.rubyforge_project = "deathbycaptcha"
|
19
|
+
|
20
|
+
s.files = `git ls-files`.split("\n")
|
21
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
22
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
23
|
+
s.require_paths = ["lib"]
|
24
|
+
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
require 'logger'
|
2
|
+
require 'digest/sha1'
|
3
|
+
require 'rest_client'
|
4
|
+
|
5
|
+
module DeathByCaptcha
|
6
|
+
|
7
|
+
#
|
8
|
+
# DeathByCaptcha API Client
|
9
|
+
#
|
10
|
+
class Client
|
11
|
+
|
12
|
+
attr_accessor :config
|
13
|
+
|
14
|
+
def initialize(username, password, extra={})
|
15
|
+
data = {
|
16
|
+
:is_verbose => false, # If true, prints messages during execution
|
17
|
+
:logger_output => STDOUT, # Logger output path or IO instance
|
18
|
+
:api_version => API_VERSION, # API version (used as user-agent with http requests)
|
19
|
+
:software_vendor_id => 0, # API unique software ID
|
20
|
+
:max_captcha_file_size => 64 * 1024, # Maximum CAPTCHA image filesize, currently 64K
|
21
|
+
:default_timeout => 60, # Default CAPTCHA timeout
|
22
|
+
:polls_interval => 5, # Default decode polling interval
|
23
|
+
:http_base_url => 'http://api.deathbycaptcha.com/api', # Base HTTP API url
|
24
|
+
:http_response_type => 'application/json', # Preferred HTTP API server's response content type, do not change
|
25
|
+
:socket_host => 'api.deathbycaptcha.com', # Socket API server's host
|
26
|
+
:socket_port => (8123..8130).map {|p| p}, # Socket API server's ports range
|
27
|
+
:username => username, # DeathByCaptcha username
|
28
|
+
:password_hashed => Digest::SHA1.hexdigest(password), # DeathByCaptcha user's password encrypted
|
29
|
+
:password => '', # DeathByCaptcha user's password not encrypted
|
30
|
+
:password_is_hashed => true # True, if the password is hashed
|
31
|
+
}.merge(extra)
|
32
|
+
|
33
|
+
@config = DeathByCaptcha::Config.new(data) # Config instance
|
34
|
+
@logger = Logger.new(@config.logger_output) # Logger
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
#
|
39
|
+
# Fetch the user's details -- balance, rate and banned status
|
40
|
+
#
|
41
|
+
def get_user
|
42
|
+
raise DeathByCaptcha::Errors::NotImplemented
|
43
|
+
end
|
44
|
+
|
45
|
+
#
|
46
|
+
# Fetch the user's balance (in US cents)
|
47
|
+
#
|
48
|
+
def get_balance
|
49
|
+
raise DeathByCaptcha::Errors::NotImplemented
|
50
|
+
end
|
51
|
+
|
52
|
+
#
|
53
|
+
# Fetch a CAPTCHA details -- its numeric ID, text and correctness
|
54
|
+
#
|
55
|
+
def get_captcha(cid)
|
56
|
+
raise DeathByCaptcha::Errors::NotImplemented
|
57
|
+
end
|
58
|
+
|
59
|
+
#
|
60
|
+
# Fetch a CAPTCHA text
|
61
|
+
#
|
62
|
+
def get_text(cid)
|
63
|
+
raise DeathByCaptcha::Errors::NotImplemented
|
64
|
+
end
|
65
|
+
|
66
|
+
#
|
67
|
+
# Report a CAPTCHA as incorrectly solved
|
68
|
+
#
|
69
|
+
def report(cid)
|
70
|
+
raise DeathByCaptcha::Errors::NotImplemented
|
71
|
+
end
|
72
|
+
|
73
|
+
#
|
74
|
+
# Remove an unsolved CAPTCHA
|
75
|
+
#
|
76
|
+
def remove(cid)
|
77
|
+
raise DeathByCaptcha::Errors::NotImplemented
|
78
|
+
end
|
79
|
+
|
80
|
+
#
|
81
|
+
# Upload a CAPTCHA
|
82
|
+
#
|
83
|
+
# Accepts file names, file objects or urls, and an optional flag telling
|
84
|
+
# whether the CAPTCHA is case-sensitive or not. Returns CAPTCHA details
|
85
|
+
# on success.
|
86
|
+
#
|
87
|
+
def upload(captcha, options={})
|
88
|
+
raise DeathByCaptcha::Errors::NotImplemented
|
89
|
+
end
|
90
|
+
|
91
|
+
#
|
92
|
+
# Try to solve a CAPTCHA.
|
93
|
+
#
|
94
|
+
# See Client.upload() for arguments details.
|
95
|
+
#
|
96
|
+
# Uploads a CAPTCHA, polls for its status periodically with arbitrary
|
97
|
+
# timeout (in seconds). Removes unsolved CAPTCHAs. Returns CAPTCHA
|
98
|
+
# details if (correctly) solved.
|
99
|
+
#
|
100
|
+
def decode(captcha, options={})
|
101
|
+
options = {:timeout => config.default_timeout, :is_case_sensitive => false, :is_raw_content => false}.merge(options)
|
102
|
+
deadline = Time.now + options[:timeout]
|
103
|
+
c = upload(captcha, options)
|
104
|
+
if c
|
105
|
+
while deadline > Time.now and (c['text'].nil? or c['text'].empty?)
|
106
|
+
sleep(config.polls_interval)
|
107
|
+
c = get_captcha(c['captcha'])
|
108
|
+
end
|
109
|
+
|
110
|
+
if c['text']
|
111
|
+
if c['is_correct']
|
112
|
+
return c
|
113
|
+
end
|
114
|
+
else
|
115
|
+
remove(c['captcha'])
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
119
|
+
|
120
|
+
|
121
|
+
end
|
122
|
+
|
123
|
+
#
|
124
|
+
# Protected methods.
|
125
|
+
#
|
126
|
+
protected
|
127
|
+
|
128
|
+
#
|
129
|
+
# Return a hash with the user's credentials
|
130
|
+
#
|
131
|
+
def userpwd
|
132
|
+
if config.password_is_hashed
|
133
|
+
return {:username => config.username, :password => config.password_hashed, :is_hashed => '1'}
|
134
|
+
else
|
135
|
+
return {:username => config.username, :password => config.password}
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
#
|
140
|
+
# Private methods.
|
141
|
+
#
|
142
|
+
private
|
143
|
+
|
144
|
+
#
|
145
|
+
# Log a command and a message
|
146
|
+
#
|
147
|
+
def log(cmd, msg='')
|
148
|
+
if @config.is_verbose
|
149
|
+
@logger.info "#{cmd}: #{msg}"
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
#
|
154
|
+
# Return the File instance that corresponds to the captcha
|
155
|
+
#
|
156
|
+
# captcha can be:
|
157
|
+
# => a raw file content if is_raw_content is true
|
158
|
+
# => a File if its kind of File
|
159
|
+
# => a url if it's a String and starts with 'http://'
|
160
|
+
# => a filesystem path otherwise
|
161
|
+
#
|
162
|
+
def load_file(captcha, is_raw_content=false)
|
163
|
+
file = nil
|
164
|
+
if is_raw_content
|
165
|
+
# Create a temporary file, write the raw content and return it
|
166
|
+
tmp_file_path = File.join(Dir.tmpdir, "captcha_#{Time.now.to_i}_#{rand}")
|
167
|
+
File.open(tmp_file_path, 'w') {|f| f.write captcha}
|
168
|
+
file = File.open(tmp_file_path, 'r')
|
169
|
+
|
170
|
+
elsif captcha.kind_of? File
|
171
|
+
# simply return the file
|
172
|
+
file = captcha
|
173
|
+
|
174
|
+
elsif captcha.kind_of? String and captcha[0,7] == 'http://'
|
175
|
+
# Create a temporary file, download the file, write it to tempfile and return it
|
176
|
+
tmp_file_path = File.join(Dir.tmpdir, "captcha_#{Time.now.to_i}_#{rand}")
|
177
|
+
File.open(tmp_file_path, 'w') {|f| f.write RestClient.get(captcha)}
|
178
|
+
file = File.open(tmp_file_path, 'r')
|
179
|
+
|
180
|
+
else
|
181
|
+
# Return the File opened
|
182
|
+
file = File.open(captcha, 'r')
|
183
|
+
|
184
|
+
end
|
185
|
+
|
186
|
+
if file.nil?
|
187
|
+
raise DeathByCaptcha::Errors::CaptchaEmpty
|
188
|
+
elsif config.max_captcha_file_size <= file.size
|
189
|
+
raise DeathByCaptcha::Errors::CaptchaOverflow
|
190
|
+
end
|
191
|
+
|
192
|
+
return file
|
193
|
+
end
|
194
|
+
|
195
|
+
end
|
196
|
+
|
197
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module DeathByCaptcha
|
2
|
+
|
3
|
+
#
|
4
|
+
# Config class
|
5
|
+
# based on http://mjijackson.com/2010/02/flexible-ruby-config-objects
|
6
|
+
#
|
7
|
+
class Config
|
8
|
+
|
9
|
+
def initialize(data={})
|
10
|
+
@data = {}
|
11
|
+
update!(data)
|
12
|
+
end
|
13
|
+
|
14
|
+
def update!(data)
|
15
|
+
data.each do |key, value|
|
16
|
+
self[key] = value
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def [](key)
|
21
|
+
@data[key.to_sym]
|
22
|
+
end
|
23
|
+
|
24
|
+
def []=(key, value)
|
25
|
+
if value.class == Hash
|
26
|
+
@data[key.to_sym] = Config.new(value)
|
27
|
+
else
|
28
|
+
@data[key.to_sym] = value
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def method_missing(sym, *args)
|
33
|
+
if sym.to_s =~ /(.+)=$/
|
34
|
+
self[$1] = args.first
|
35
|
+
else
|
36
|
+
self[sym]
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module DeathByCaptcha
|
2
|
+
module Errors
|
3
|
+
|
4
|
+
|
5
|
+
#
|
6
|
+
# Custom Error class for rescuing from DeathByCaptcha API errors
|
7
|
+
#
|
8
|
+
class Error < StandardError
|
9
|
+
|
10
|
+
def initialize(message)
|
11
|
+
super "#{message} (DEATHBYCAPTCHA API ERROR)"
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
#
|
17
|
+
# Raised when a method tries to access a not implemented method
|
18
|
+
#
|
19
|
+
class NotImplemented < Error
|
20
|
+
def initialize
|
21
|
+
super 'The requested functionality was not implemented'
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
#
|
26
|
+
# Raised when a HTTP call fails
|
27
|
+
#
|
28
|
+
class CallError < Error
|
29
|
+
def initialize
|
30
|
+
super 'HTTP call failed'
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
#
|
35
|
+
# Raised when the user is not allowed to access the API
|
36
|
+
#
|
37
|
+
class AccessDenied < Error
|
38
|
+
def initialize
|
39
|
+
super 'Access denied, please check your credentials and/or balance'
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
#
|
44
|
+
# Raised when the captcha file could not be loaded or is empty
|
45
|
+
#
|
46
|
+
class CaptchaEmpty
|
47
|
+
def initialize
|
48
|
+
super 'CAPTCHA image is empty or could not be loaded'
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
#
|
53
|
+
# Raised when the size of the captcha file is too big
|
54
|
+
#
|
55
|
+
class CaptchaOverflow
|
56
|
+
def initialize
|
57
|
+
super 'CAPTCHA image is too big'
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'rest_client'
|
2
|
+
require 'json'
|
3
|
+
require 'digest/md5'
|
4
|
+
|
5
|
+
module DeathByCaptcha
|
6
|
+
|
7
|
+
#
|
8
|
+
# DeathByCaptcha HTTP API client
|
9
|
+
#
|
10
|
+
class HTTPClient < DeathByCaptcha::Client
|
11
|
+
|
12
|
+
def get_user
|
13
|
+
call('user', userpwd)
|
14
|
+
end
|
15
|
+
|
16
|
+
def get_captcha(cid)
|
17
|
+
call("captcha/#{cid}")
|
18
|
+
end
|
19
|
+
|
20
|
+
def report(cid)
|
21
|
+
call("captcha/#{cid}/report", userpwd)['is_correct']
|
22
|
+
end
|
23
|
+
|
24
|
+
def remove(cid)
|
25
|
+
not call("captcha/#{cid}/remove", userpwd)['captcha']
|
26
|
+
end
|
27
|
+
|
28
|
+
def a(nome, test=false, valor="verdade")
|
29
|
+
puts nome,test,valor
|
30
|
+
end
|
31
|
+
|
32
|
+
#
|
33
|
+
# Protected methods.
|
34
|
+
#
|
35
|
+
protected
|
36
|
+
|
37
|
+
def upload(captcha, options={})
|
38
|
+
options = {:is_case_sensitive => false, :is_raw_content => false}.merge(options)
|
39
|
+
data = userpwd
|
40
|
+
data[:swid] = config.software_vendor_id
|
41
|
+
data[:is_case_sensitive] = options[:is_case_sensitive] ? 1 : 0
|
42
|
+
data[:captchafile] = load_file(captcha, options[:is_raw_content])
|
43
|
+
response = call('captcha', data)
|
44
|
+
return response if response['captcha']
|
45
|
+
end
|
46
|
+
|
47
|
+
#
|
48
|
+
# Private methods.
|
49
|
+
#
|
50
|
+
private
|
51
|
+
|
52
|
+
def call(cmd, payload={}, headers={})
|
53
|
+
headers['Accept'] = config.http_response_type if headers['Accept'].nil?
|
54
|
+
headers['User-Agent'] = config.api_version if headers['User-Agent'].nil?
|
55
|
+
|
56
|
+
log('SEND', "#{cmd} #{payload}")
|
57
|
+
|
58
|
+
begin
|
59
|
+
url = "#{config.http_base_url}/#{cmd}"
|
60
|
+
|
61
|
+
if payload.empty?
|
62
|
+
response = RestClient.get(url, headers)
|
63
|
+
else
|
64
|
+
response = RestClient.post(url, payload, headers)
|
65
|
+
end
|
66
|
+
|
67
|
+
log('RECV', "#{response.size} #{response}")
|
68
|
+
|
69
|
+
return JSON.load(response)
|
70
|
+
|
71
|
+
rescue RestClient::Unauthorized => exc
|
72
|
+
raise DeathByCaptcha::Errors::AccessDenied
|
73
|
+
|
74
|
+
rescue RestClient::RequestFailed => exc
|
75
|
+
raise DeathByCaptcha::Errors::AccessDenied
|
76
|
+
|
77
|
+
else
|
78
|
+
raise DeathByCaptcha::Errors::CallError
|
79
|
+
|
80
|
+
end
|
81
|
+
|
82
|
+
return {}
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
def self.http_client(username, password, extra={})
|
90
|
+
DeathByCaptcha::HTTPClient.new(username, password, extra)
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
@@ -0,0 +1,249 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'digest/md5'
|
3
|
+
require 'thread'
|
4
|
+
require 'socket'
|
5
|
+
require 'base64'
|
6
|
+
|
7
|
+
module DeathByCaptcha
|
8
|
+
|
9
|
+
#
|
10
|
+
# DeathByCaptcha Socket API client
|
11
|
+
#
|
12
|
+
class SocketClient < DeathByCaptcha::Client
|
13
|
+
|
14
|
+
#
|
15
|
+
# Socket API server's host & ports range.
|
16
|
+
#
|
17
|
+
@@socket_host = 'api.deathbycaptcha.com'
|
18
|
+
@@socket_ports = (8123...8131).to_a
|
19
|
+
|
20
|
+
def initialize(username, password, extra = {})
|
21
|
+
@mutex = Mutex.new
|
22
|
+
@socket = nil
|
23
|
+
|
24
|
+
super(username, password, extra)
|
25
|
+
end
|
26
|
+
|
27
|
+
def get_user
|
28
|
+
call('user', userpwd)
|
29
|
+
end
|
30
|
+
|
31
|
+
def get_captcha(cid)
|
32
|
+
call('captcha', {:captcha => cid})
|
33
|
+
end
|
34
|
+
|
35
|
+
def report(cid)
|
36
|
+
call("captcha/#{cid}/report", userpwd)[:is_correct]
|
37
|
+
|
38
|
+
data = userpwd
|
39
|
+
data['captcha'] = cid
|
40
|
+
not call('report', data)[:is_correct]
|
41
|
+
end
|
42
|
+
|
43
|
+
def remove(cid)
|
44
|
+
not call("captcha/#{cid}/remove", userpwd)[:captcha]
|
45
|
+
|
46
|
+
data = userpwd
|
47
|
+
data['captcha'] = cid
|
48
|
+
not call('remove', data)[:captcha]
|
49
|
+
end
|
50
|
+
|
51
|
+
#
|
52
|
+
# Protected methods.
|
53
|
+
#
|
54
|
+
protected
|
55
|
+
|
56
|
+
def upload(captcha, is_case_sensitive=false, is_raw_content=false)
|
57
|
+
data = userpwd
|
58
|
+
data[:captcha] = Base64.encode64(load_file(captcha, is_raw_content).read)
|
59
|
+
|
60
|
+
data[:is_case_sensitive] = is_case_sensitive ? 1 : 0
|
61
|
+
response = call('upload', data)
|
62
|
+
|
63
|
+
response
|
64
|
+
end
|
65
|
+
|
66
|
+
#
|
67
|
+
# Private methods.
|
68
|
+
#
|
69
|
+
private
|
70
|
+
|
71
|
+
def connect
|
72
|
+
unless @socket
|
73
|
+
log('CONN')
|
74
|
+
|
75
|
+
begin
|
76
|
+
random_port = @@socket_ports[rand(@@socket_ports.size)]
|
77
|
+
|
78
|
+
# Creates a new Socket.
|
79
|
+
addr = Socket.pack_sockaddr_in(random_port, @@socket_host)
|
80
|
+
|
81
|
+
@socket = Socket.new(:INET, :STREAM)
|
82
|
+
@socket.connect_nonblock(addr)
|
83
|
+
rescue Exception => e
|
84
|
+
if e.errno == 36 # EINPROGRESS
|
85
|
+
# Nothing.
|
86
|
+
else
|
87
|
+
close # Closes the socket.
|
88
|
+
log('CONN', 'Could not connect.')
|
89
|
+
log('CONN', e.backtrace.join('\n'))
|
90
|
+
|
91
|
+
raise e
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
@socket
|
98
|
+
end
|
99
|
+
|
100
|
+
def close
|
101
|
+
if @socket
|
102
|
+
log('CLOSE')
|
103
|
+
|
104
|
+
begin
|
105
|
+
@socket.close
|
106
|
+
rescue Exception => e
|
107
|
+
log('CLOSE', 'Could not close socket.')
|
108
|
+
log('CLOSE', e.backtrace.join('\n'))
|
109
|
+
ensure
|
110
|
+
@socket = nil
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def send(sock, buf)
|
116
|
+
# buf += '\n'
|
117
|
+
fds = [sock]
|
118
|
+
|
119
|
+
deadline = Time.now.to_f + 3 * config.polls_interval
|
120
|
+
while deadline > Time.now.to_f and not buf.empty? do
|
121
|
+
_, wr, ex = IO.select([], fds, fds, config.polls_interval)
|
122
|
+
|
123
|
+
if ex and ex.any?
|
124
|
+
raise IOError.new('send(): select() excepted')
|
125
|
+
elsif wr
|
126
|
+
while buf and not buf.empty? do
|
127
|
+
begin
|
128
|
+
sent = wr.first.send(buf, 0)
|
129
|
+
buf = buf[sent, buf.size - sent]
|
130
|
+
rescue Exception => e
|
131
|
+
if [35, 36].include? e.errno
|
132
|
+
break
|
133
|
+
else
|
134
|
+
raise e
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
unless buf.empty?
|
142
|
+
raise IOError.new('send() timed out')
|
143
|
+
else
|
144
|
+
return self
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def recv(sock)
|
149
|
+
fds = [sock]
|
150
|
+
buf = ''
|
151
|
+
|
152
|
+
deadline = Time.now.to_f() + 3 * config.polls_interval
|
153
|
+
while deadline > Time.now.to_f do
|
154
|
+
rd, _, ex = IO.select(fds, [], fds, config.polls_interval)
|
155
|
+
|
156
|
+
if ex and ex.any?
|
157
|
+
raise IOError.new('send(): select() excepted')
|
158
|
+
elsif rd
|
159
|
+
while true do
|
160
|
+
begin
|
161
|
+
s = rd.first.recv_nonblock(256)
|
162
|
+
rescue Exception => e
|
163
|
+
if [35, 36].include? e.errno
|
164
|
+
break
|
165
|
+
else
|
166
|
+
raise e
|
167
|
+
end
|
168
|
+
else
|
169
|
+
if not s
|
170
|
+
raise IOError.new('recv(): connection lost')
|
171
|
+
else
|
172
|
+
buf += s
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
break if buf.size > 0
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
return buf[0, buf.size - 1] if buf.size > 0
|
182
|
+
raise IOError.new('recv() timed out')
|
183
|
+
end
|
184
|
+
|
185
|
+
def call(cmd, data = {})
|
186
|
+
data = {} if data.nil?
|
187
|
+
data.merge!({:cmd => cmd, :version => config.api_version})
|
188
|
+
|
189
|
+
request = data.to_json
|
190
|
+
log('SEND', request.to_s)
|
191
|
+
|
192
|
+
response = nil
|
193
|
+
|
194
|
+
(0...1).each do
|
195
|
+
# Locks other threads.
|
196
|
+
# If another thread has already acquired the lock, this thread will be locked.
|
197
|
+
@mutex.lock
|
198
|
+
|
199
|
+
begin
|
200
|
+
sock = connect
|
201
|
+
send(sock, request)
|
202
|
+
|
203
|
+
response = recv(sock)
|
204
|
+
rescue Exception => e
|
205
|
+
log('SEND', e.message)
|
206
|
+
log('SEND', e.backtrace.join('\n'))
|
207
|
+
close
|
208
|
+
else
|
209
|
+
# If no exception raised.
|
210
|
+
break
|
211
|
+
ensure
|
212
|
+
@mutex.unlock
|
213
|
+
end
|
214
|
+
|
215
|
+
end
|
216
|
+
|
217
|
+
if response.nil?
|
218
|
+
msg = 'Connection timed out during API request'
|
219
|
+
log('SEND', msg)
|
220
|
+
|
221
|
+
raise Exception.new(msg)
|
222
|
+
end
|
223
|
+
|
224
|
+
log('RECV', response.to_s)
|
225
|
+
|
226
|
+
begin
|
227
|
+
response = JSON.load(response)
|
228
|
+
rescue Exception => e
|
229
|
+
raise Exception.new('Invalid API response')
|
230
|
+
end
|
231
|
+
|
232
|
+
if 0x00 < response['status'] and 0x10 > response['status']
|
233
|
+
raise DeathByCaptcha::Errors::AccessDenied
|
234
|
+
elsif 0xff == response['status']
|
235
|
+
raise Exception.new('API server error occured')
|
236
|
+
else
|
237
|
+
return response
|
238
|
+
end
|
239
|
+
|
240
|
+
end
|
241
|
+
|
242
|
+
end
|
243
|
+
|
244
|
+
|
245
|
+
def self.socket_client(username, password, extra={})
|
246
|
+
DeathByCaptcha::SocketClient.new(username, password, extra)
|
247
|
+
end
|
248
|
+
|
249
|
+
end
|
metadata
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: deathbycaptcha
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 4
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
version: 4.0.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Rafael Barbolo Lopes, Rafael Ivan Garcia
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2011-04-07 00:00:00 -03:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: rest-client
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ~>
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
segments:
|
29
|
+
- 1
|
30
|
+
- 6
|
31
|
+
- 1
|
32
|
+
version: 1.6.1
|
33
|
+
type: :runtime
|
34
|
+
version_requirements: *id001
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: json
|
37
|
+
prerelease: false
|
38
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ~>
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
segments:
|
44
|
+
- 1
|
45
|
+
- 4
|
46
|
+
- 6
|
47
|
+
version: 1.4.6
|
48
|
+
type: :runtime
|
49
|
+
version_requirements: *id002
|
50
|
+
description: Ruby API for DeathByCaptcha (Captcha Solver as a Service)
|
51
|
+
email:
|
52
|
+
- tech@infosimples.com.br
|
53
|
+
executables: []
|
54
|
+
|
55
|
+
extensions: []
|
56
|
+
|
57
|
+
extra_rdoc_files: []
|
58
|
+
|
59
|
+
files:
|
60
|
+
- .gitignore
|
61
|
+
- Gemfile
|
62
|
+
- MIT-LICENSE
|
63
|
+
- README.rdoc
|
64
|
+
- Rakefile
|
65
|
+
- deathbycaptcha.gemspec
|
66
|
+
- lib/deathbycaptcha.rb
|
67
|
+
- lib/deathbycaptcha/client.rb
|
68
|
+
- lib/deathbycaptcha/config.rb
|
69
|
+
- lib/deathbycaptcha/error.rb
|
70
|
+
- lib/deathbycaptcha/http_client.rb
|
71
|
+
- lib/deathbycaptcha/socket_client.rb
|
72
|
+
- lib/deathbycaptcha/version.rb
|
73
|
+
has_rdoc: true
|
74
|
+
homepage: http://github.com/infosimples/deathbycaptcha
|
75
|
+
licenses: []
|
76
|
+
|
77
|
+
post_install_message:
|
78
|
+
rdoc_options: []
|
79
|
+
|
80
|
+
require_paths:
|
81
|
+
- lib
|
82
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ">="
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
segments:
|
88
|
+
- 0
|
89
|
+
version: "0"
|
90
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
91
|
+
none: false
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
segments:
|
96
|
+
- 0
|
97
|
+
version: "0"
|
98
|
+
requirements: []
|
99
|
+
|
100
|
+
rubyforge_project: deathbycaptcha
|
101
|
+
rubygems_version: 1.3.7
|
102
|
+
signing_key:
|
103
|
+
specification_version: 3
|
104
|
+
summary: Ruby API for DeathByCaptcha (Captcha Solver as a Service)
|
105
|
+
test_files: []
|
106
|
+
|