deathbycaptcha 4.1.5 → 5.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -4
- data/CHANGELOG.md +20 -0
- data/Gemfile +1 -1
- data/{MIT-LICENSE → LICENSE.txt} +4 -2
- data/README.md +132 -51
- data/Rakefile +2 -2
- data/captchas/1.png +0 -0
- data/deathbycaptcha.gemspec +21 -21
- data/lib/deathbycaptcha.rb +11 -6
- data/lib/deathbycaptcha/client.rb +143 -129
- data/lib/deathbycaptcha/client/http.rb +135 -0
- data/lib/deathbycaptcha/client/socket.rb +113 -0
- data/lib/deathbycaptcha/exceptions.rb +62 -0
- data/lib/deathbycaptcha/models.rb +16 -0
- data/lib/deathbycaptcha/models/captcha.rb +20 -0
- data/lib/deathbycaptcha/models/server_status.rb +20 -0
- data/lib/deathbycaptcha/models/user.rb +28 -0
- data/lib/deathbycaptcha/patches.rb +9 -0
- data/lib/deathbycaptcha/version.rb +3 -3
- data/spec/credentials.yml.example +3 -0
- data/spec/lib/client_spec.rb +94 -0
- data/spec/spec_helper.rb +21 -0
- metadata +59 -39
- data/lib/deathbycaptcha/config.rb +0 -42
- data/lib/deathbycaptcha/errors.rb +0 -69
- data/lib/deathbycaptcha/http_client.rb +0 -98
- data/lib/deathbycaptcha/socket_client.rb +0 -224
@@ -0,0 +1,135 @@
|
|
1
|
+
module DeathByCaptcha
|
2
|
+
|
3
|
+
# HTTP client for DeathByCaptcha API.
|
4
|
+
#
|
5
|
+
class Client::HTTP < Client
|
6
|
+
|
7
|
+
BASE_URL = 'http://api.dbcapi.me/api'
|
8
|
+
|
9
|
+
# Retrieve information from an uploaded captcha.
|
10
|
+
#
|
11
|
+
# @param [Integer] captcha_id Numeric ID of the captcha.
|
12
|
+
#
|
13
|
+
# @return [DeathByCaptcha::Captcha] The captcha object.
|
14
|
+
#
|
15
|
+
def captcha(captcha_id)
|
16
|
+
response = perform("captcha/#{captcha_id}")
|
17
|
+
DeathByCaptcha::Captcha.new(response)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Report incorrectly solved captcha for refund.
|
21
|
+
#
|
22
|
+
# @param [Integer] captcha_id Numeric ID of the captcha.
|
23
|
+
#
|
24
|
+
# @return [DeathByCaptcha::Captcha] The captcha object.
|
25
|
+
#
|
26
|
+
def report!(captcha_id)
|
27
|
+
response = perform("captcha/#{captcha_id}/report", :post)
|
28
|
+
DeathByCaptcha::Captcha.new(response)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Retrieve your user information (which has the current credit balance)
|
32
|
+
#
|
33
|
+
# @return [DeathByCaptcha::User] The user object.
|
34
|
+
#
|
35
|
+
def user
|
36
|
+
response = perform('user')
|
37
|
+
DeathByCaptcha::User.new(response)
|
38
|
+
end
|
39
|
+
|
40
|
+
# Retrieve DeathByCaptcha server status.
|
41
|
+
#
|
42
|
+
# @return [DeathByCaptcha::ServerStatus] The server status object.
|
43
|
+
#
|
44
|
+
def status
|
45
|
+
response = perform('status')
|
46
|
+
DeathByCaptcha::ServerStatus.new(response)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Upload a captcha to DeathByCaptcha.
|
50
|
+
#
|
51
|
+
# This method will not return the solution. It's only useful if you want to
|
52
|
+
# implement your own "decode" function.
|
53
|
+
#
|
54
|
+
# @return [DeathByCaptcha::Captcha] The captcha object (not solved yet).
|
55
|
+
#
|
56
|
+
def upload(raw64)
|
57
|
+
response = perform('captcha', :post_multipart, captchafile: "base64:#{raw64}")
|
58
|
+
DeathByCaptcha::Captcha.new(response)
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
# Perform an HTTP request to the DeathByCaptcha API.
|
64
|
+
#
|
65
|
+
# @param [String] action API method name.
|
66
|
+
# @param [Symbol] method HTTP method (:get, :post, :post_multipart).
|
67
|
+
# @param [Hash] payload Data to be sent through the HTTP request.
|
68
|
+
#
|
69
|
+
# @return [Hash] Response from the DeathByCaptcha API.
|
70
|
+
#
|
71
|
+
def perform(action, method = :get, payload = {})
|
72
|
+
payload.merge!(username: self.username, password: self.password)
|
73
|
+
|
74
|
+
headers = { 'User-Agent' => DeathByCaptcha::API_VERSION }
|
75
|
+
|
76
|
+
if method == :post
|
77
|
+
uri = URI("#{BASE_URL}/#{action}")
|
78
|
+
req = Net::HTTP::Post.new(uri.request_uri, headers)
|
79
|
+
req.set_form_data(payload)
|
80
|
+
|
81
|
+
elsif method == :post_multipart
|
82
|
+
uri = URI("#{BASE_URL}/#{action}")
|
83
|
+
req = Net::HTTP::Post.new(uri.request_uri, headers)
|
84
|
+
boundary, body = prepare_multipart_data(payload)
|
85
|
+
req.content_type = "multipart/form-data; boundary=#{boundary}"
|
86
|
+
req.body = body
|
87
|
+
|
88
|
+
else
|
89
|
+
uri = URI("#{BASE_URL}/#{action}?#{URI.encode_www_form(payload)}")
|
90
|
+
req = Net::HTTP::Get.new(uri.request_uri, headers)
|
91
|
+
end
|
92
|
+
|
93
|
+
res = Net::HTTP.start(uri.hostname, uri.port) do |http|
|
94
|
+
http.request(req)
|
95
|
+
end
|
96
|
+
|
97
|
+
case res
|
98
|
+
when Net::HTTPSuccess, Net::HTTPSeeOther
|
99
|
+
Hash[URI.decode_www_form(res.body)]
|
100
|
+
when Net::HTTPForbidden
|
101
|
+
raise DeathByCaptcha::APIForbidden
|
102
|
+
when Net::HTTPBadRequest
|
103
|
+
raise DeathByCaptcha::APIBadRequest
|
104
|
+
when Net::HTTPRequestEntityTooLarge
|
105
|
+
raise DeathByCaptcha::APICaptchaTooLarge
|
106
|
+
when Net::HTTPServiceUnavailable
|
107
|
+
raise DeathByCaptcha::APIServiceUnavailable
|
108
|
+
else
|
109
|
+
raise DeathByCaptcha::APIResponseError.new(res.body)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
# Prepare the multipart data to be sent via a :post_multipart request.
|
114
|
+
#
|
115
|
+
# @param [Hash] payload Data to be prepared via a multipart post.
|
116
|
+
#
|
117
|
+
# @return [String,String] Boundary and body for the multipart post.
|
118
|
+
#
|
119
|
+
def prepare_multipart_data(payload)
|
120
|
+
boundary = "infosimples" + rand(1_000_000).to_s # a random unique string
|
121
|
+
|
122
|
+
content = []
|
123
|
+
payload.each do |param, value|
|
124
|
+
content << '--' + boundary
|
125
|
+
content << "Content-Disposition: form-data; name=\"#{param}\""
|
126
|
+
content << ''
|
127
|
+
content << value
|
128
|
+
end
|
129
|
+
content << '--' + boundary + '--'
|
130
|
+
content << ''
|
131
|
+
|
132
|
+
[boundary, content.join("\r\n")]
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
module DeathByCaptcha
|
2
|
+
|
3
|
+
# Socket client for DeathByCaptcha API.
|
4
|
+
#
|
5
|
+
class Client::Socket < Client
|
6
|
+
|
7
|
+
HOST = 'api.dbcapi.me'
|
8
|
+
PORTS = (8123..8130).to_a
|
9
|
+
|
10
|
+
# Retrieve information from an uploaded captcha.
|
11
|
+
#
|
12
|
+
# @param [Integer] captcha_id Numeric ID of the captcha.
|
13
|
+
#
|
14
|
+
# @return [DeathByCaptcha::Captcha] The captcha object.
|
15
|
+
#
|
16
|
+
def captcha(captcha_id)
|
17
|
+
response = perform('captcha', captcha: captcha_id)
|
18
|
+
DeathByCaptcha::Captcha.new(response)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Report incorrectly solved captcha for refund.
|
22
|
+
#
|
23
|
+
# @param [Integer] captcha_id Numeric ID of the captcha.
|
24
|
+
#
|
25
|
+
# @return [DeathByCaptcha::Captcha] The captcha object.
|
26
|
+
#
|
27
|
+
def report!(captcha_id)
|
28
|
+
response = perform('report', captcha: captcha_id)
|
29
|
+
DeathByCaptcha::Captcha.new(response)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Retrieve your user information (which has the current credit balance)
|
33
|
+
#
|
34
|
+
# @return [DeathByCaptcha::User] The user object.
|
35
|
+
#
|
36
|
+
def user
|
37
|
+
response = perform('user')
|
38
|
+
DeathByCaptcha::User.new(response)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Retrieve DeathByCaptcha server status. This method won't use a Socket
|
42
|
+
# connection, it will use an HTTP connection.
|
43
|
+
#
|
44
|
+
# @return [DeathByCaptcha::ServerStatus] The server status object.
|
45
|
+
#
|
46
|
+
def status
|
47
|
+
http_client.status
|
48
|
+
end
|
49
|
+
|
50
|
+
# Upload a captcha to DeathByCaptcha.
|
51
|
+
#
|
52
|
+
# This method will not return the solution. It's only useful if you want to
|
53
|
+
# implement your own "decode" function.
|
54
|
+
#
|
55
|
+
# @return [DeathByCaptcha::Captcha] The captcha object (not solved yet).
|
56
|
+
#
|
57
|
+
def upload(raw64)
|
58
|
+
response = perform('upload', captcha: raw64)
|
59
|
+
DeathByCaptcha::Captcha.new(response)
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
# Perform a Socket communication with the DeathByCaptcha API.
|
65
|
+
#
|
66
|
+
# @param [String] action API method name.
|
67
|
+
# @param [Hash] payload Data to be exchanged in the communication.
|
68
|
+
#
|
69
|
+
# @return [Hash] Response from the DeathByCaptcha API.
|
70
|
+
#
|
71
|
+
def perform(action, payload = {})
|
72
|
+
payload.merge!(
|
73
|
+
cmd: action,
|
74
|
+
version: DeathByCaptcha::API_VERSION,
|
75
|
+
username: self.username,
|
76
|
+
password: self.password
|
77
|
+
)
|
78
|
+
|
79
|
+
response = ::Socket.tcp(HOST, PORTS.sample) do |socket|
|
80
|
+
socket.puts payload.to_json
|
81
|
+
socket.read
|
82
|
+
end
|
83
|
+
|
84
|
+
begin
|
85
|
+
response = JSON.parse(response)
|
86
|
+
rescue
|
87
|
+
raise DeathByCaptcha::APIResponseError.new("invalid JSON: #{response}")
|
88
|
+
end
|
89
|
+
|
90
|
+
if !(error = response['error'].to_s).empty?
|
91
|
+
case error
|
92
|
+
when 'not-logged-in', 'invalid-credentials', 'banned', 'insufficient-funds'
|
93
|
+
raise DeathByCaptcha::APIForbidden
|
94
|
+
when 'invalid-captcha'
|
95
|
+
raise DeathByCaptcha::APIBadRequest
|
96
|
+
when 'service-overload'
|
97
|
+
raise DeathByCaptcha::APIServiceUnavailable
|
98
|
+
else
|
99
|
+
raise DeathByCaptcha::APIResponseError.new(error)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
response
|
104
|
+
end
|
105
|
+
|
106
|
+
# Return a cached http client for methods that doesn't work with sockets.
|
107
|
+
#
|
108
|
+
def http_client
|
109
|
+
@http_client ||= DeathByCaptcha.new(self.username, self.password, :http)
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module DeathByCaptcha
|
2
|
+
|
3
|
+
# This is the base DeathByCaptcha exception class. Rescue it if you want to
|
4
|
+
# catch any exception that might be raised.
|
5
|
+
#
|
6
|
+
class Error < Exception
|
7
|
+
end
|
8
|
+
|
9
|
+
class InvalidClientConnection < Error
|
10
|
+
def initialize
|
11
|
+
super('You have specified an invalid client connection (valid connections are :socket, :http)')
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class InvalidCaptcha < Error
|
16
|
+
def initialize
|
17
|
+
super('The captcha is empty or invalid')
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
class Timeout < Error
|
22
|
+
def initialize
|
23
|
+
super('The captcha was not solved in the expected time')
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class IncorrectSolution < Error
|
28
|
+
def initialize
|
29
|
+
super('CAPTCHA was not solved by DeathByCaptcha. Try again.')
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
class APIResponseError < Error
|
34
|
+
def initialize(info)
|
35
|
+
super("Invalid API response: #{info}")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class APIForbidden < Error
|
40
|
+
def initialize
|
41
|
+
super('Access denied, please check your credentials and/or balance')
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
class APIServiceUnavailable < Error
|
46
|
+
def initialize
|
47
|
+
super('CAPTCHA was rejected due to service overload, try again later')
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class APIBadRequest < Error
|
52
|
+
def initialize
|
53
|
+
super('CAPTCHA was rejected by the service, check if it\'s a valid image')
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
class APICaptchaTooLarge < Error
|
58
|
+
def initialize
|
59
|
+
super('CAPTCHA image size is too large')
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module DeathByCaptcha
|
2
|
+
|
3
|
+
# Base class of a model object returned by DeathByCaptcha API.
|
4
|
+
#
|
5
|
+
class Model
|
6
|
+
def initialize(values = {})
|
7
|
+
values.each do |key, value|
|
8
|
+
self.send("#{key}=", value) if self.respond_to?("#{key}=")
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
require 'deathbycaptcha/models/captcha'
|
15
|
+
require 'deathbycaptcha/models/server_status'
|
16
|
+
require 'deathbycaptcha/models/user'
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module DeathByCaptcha
|
2
|
+
|
3
|
+
# Model of a Captcha returned by DeathByCaptcha API.
|
4
|
+
#
|
5
|
+
class Captcha < DeathByCaptcha::Model
|
6
|
+
attr_accessor :captcha, :is_correct, :text
|
7
|
+
|
8
|
+
def id
|
9
|
+
@captcha
|
10
|
+
end
|
11
|
+
|
12
|
+
def is_correct=(value)
|
13
|
+
@is_correct = ['1', true].include?(value)
|
14
|
+
end
|
15
|
+
|
16
|
+
def captcha=(value)
|
17
|
+
@captcha = value.to_i
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module DeathByCaptcha
|
2
|
+
|
3
|
+
# Model of a server status returned by DeathByCaptcha API.
|
4
|
+
#
|
5
|
+
class ServerStatus < DeathByCaptcha::Model
|
6
|
+
attr_accessor :todays_accuracy, :solved_in, :is_service_overloaded
|
7
|
+
|
8
|
+
def todays_accuracy=(value)
|
9
|
+
@todays_accuracy = value.to_f
|
10
|
+
end
|
11
|
+
|
12
|
+
def solved_in=(value)
|
13
|
+
@solved_in = value.to_i
|
14
|
+
end
|
15
|
+
|
16
|
+
def is_service_overloaded=(value)
|
17
|
+
@is_service_overloaded = ['1', true].include?(value)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module DeathByCaptcha
|
2
|
+
|
3
|
+
# Model of a User returned by DeathByCaptcha API.
|
4
|
+
#
|
5
|
+
class User < DeathByCaptcha::Model
|
6
|
+
attr_accessor :is_banned, :balance, :rate, :user
|
7
|
+
|
8
|
+
def id
|
9
|
+
@user
|
10
|
+
end
|
11
|
+
|
12
|
+
def is_banned=(value)
|
13
|
+
@is_banned = ['1', true].include?(value)
|
14
|
+
end
|
15
|
+
|
16
|
+
def balance=(value)
|
17
|
+
@balance = value.to_f
|
18
|
+
end
|
19
|
+
|
20
|
+
def rate=(value)
|
21
|
+
@rate = value.to_f
|
22
|
+
end
|
23
|
+
|
24
|
+
def user=(value)
|
25
|
+
@user = value.to_i
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -1,4 +1,4 @@
|
|
1
1
|
module DeathByCaptcha
|
2
|
-
VERSION = "
|
3
|
-
API_VERSION = "DBC/Ruby
|
4
|
-
end
|
2
|
+
VERSION = "5.0.0"
|
3
|
+
API_VERSION = "DBC/Ruby v#{VERSION}"
|
4
|
+
end
|
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
username = CREDENTIALS['username']
|
4
|
+
password = CREDENTIALS['password']
|
5
|
+
captcha_id = CREDENTIALS['captcha_id']
|
6
|
+
image64 = Base64.encode64(File.open('captchas/1.png', 'rb').read)
|
7
|
+
|
8
|
+
describe DeathByCaptcha::Client do
|
9
|
+
describe '.create' do
|
10
|
+
context 'http' do
|
11
|
+
let(:client) { DeathByCaptcha.new(username, password, :http) }
|
12
|
+
it { expect(client).to be_a(DeathByCaptcha::Client::HTTP) }
|
13
|
+
end
|
14
|
+
|
15
|
+
context 'socket' do
|
16
|
+
let(:client) { DeathByCaptcha.new(username, password, :socket) }
|
17
|
+
it { expect(client).to be_a(DeathByCaptcha::Client::Socket) }
|
18
|
+
end
|
19
|
+
|
20
|
+
context 'default' do
|
21
|
+
let(:client) { DeathByCaptcha.new(username, password) }
|
22
|
+
it { expect(client).to be_a(DeathByCaptcha::Client::Socket) }
|
23
|
+
end
|
24
|
+
|
25
|
+
context 'other' do
|
26
|
+
it { expect {
|
27
|
+
DeathByCaptcha.new(username, password, :other)
|
28
|
+
}.to raise_error(DeathByCaptcha::InvalidClientConnection)
|
29
|
+
}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
context 'http' do
|
34
|
+
subject(:client) { DeathByCaptcha.new(username, password, :http) }
|
35
|
+
|
36
|
+
describe '#load_captcha' do
|
37
|
+
it { expect(client.send(:load_captcha, url: 'http://bit.ly/1xXZcKo')).to eq(image64) }
|
38
|
+
it { expect(client.send(:load_captcha, path: 'captchas/1.png')).to eq(image64) }
|
39
|
+
it { expect(client.send(:load_captcha, file: File.open('captchas/1.png', 'rb'))).to eq(image64) }
|
40
|
+
it { expect(client.send(:load_captcha, raw: File.open('captchas/1.png', 'rb').read)).to eq(image64) }
|
41
|
+
it { expect(client.send(:load_captcha, raw64: image64)).to eq(image64) }
|
42
|
+
it { expect(client.send(:load_captcha, other: nil)).to eq('') }
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
shared_examples 'a client' do
|
48
|
+
describe '#captcha' do
|
49
|
+
before(:all) { @captcha = @client.captcha(captcha_id) }
|
50
|
+
it { expect(@captcha).to be_a(DeathByCaptcha::Captcha) }
|
51
|
+
it { expect(@captcha.text.size).to be > 0 }
|
52
|
+
it { expect([true, false]).to include(@captcha.is_correct) }
|
53
|
+
it { expect(@captcha.id).to be > 0 }
|
54
|
+
it { expect(@captcha.id).to eq(@captcha.captcha) }
|
55
|
+
end
|
56
|
+
|
57
|
+
describe '#user' do
|
58
|
+
before(:all) { @user = @client.user() }
|
59
|
+
it { expect(@user).to be_a(DeathByCaptcha::User) }
|
60
|
+
it { expect([true, false]).to include(@user.is_banned) }
|
61
|
+
it { expect(@user.balance).to be > 0 }
|
62
|
+
it { expect(@user.rate).to be > 0 }
|
63
|
+
it { expect(@user.id).to eq(@user.user) }
|
64
|
+
end
|
65
|
+
|
66
|
+
describe '#status' do
|
67
|
+
before(:all) { @status = @client.status() }
|
68
|
+
it { expect(@status).to be_a(DeathByCaptcha::ServerStatus) }
|
69
|
+
it { expect(@status.todays_accuracy).to be > 0 }
|
70
|
+
it { expect(@status.solved_in).to be > 0 }
|
71
|
+
it { expect([true, false]).to include(@status.is_service_overloaded) }
|
72
|
+
end
|
73
|
+
|
74
|
+
describe '#decode!' do
|
75
|
+
before(:all) { @captcha = @client.decode!(raw64: image64) }
|
76
|
+
it { expect(@captcha).to be_a(DeathByCaptcha::Captcha) }
|
77
|
+
it { expect(@captcha.text).to eq 'infosimples' }
|
78
|
+
it { expect(@captcha.is_correct).to be true }
|
79
|
+
it { expect(@captcha.id).to be > 0 }
|
80
|
+
it { expect(@captcha.id).to eq(@captcha.captcha) }
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
describe DeathByCaptcha::Client::HTTP do
|
85
|
+
it_behaves_like 'a client' do
|
86
|
+
before(:all) { @client = DeathByCaptcha.new(username, password, :http) }
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
describe DeathByCaptcha::Client::Socket do
|
91
|
+
it_behaves_like 'a client' do
|
92
|
+
before(:all) { @client = DeathByCaptcha.new(username, password, :socket) }
|
93
|
+
end
|
94
|
+
end
|