private_captcha 0.0.1
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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +165 -0
- data/lib/private_captcha/client.rb +172 -0
- data/lib/private_captcha/configuration.rb +24 -0
- data/lib/private_captcha/errors.rb +50 -0
- data/lib/private_captcha/middleware.rb +41 -0
- data/lib/private_captcha/verify_output.rb +62 -0
- data/lib/private_captcha/version.rb +5 -0
- data/lib/private_captcha.rb +14 -0
- metadata +69 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 8d114ffe4accff4d744e8a31c2a4db2e19a8415536f90edb03646c84a7d00c16
|
|
4
|
+
data.tar.gz: ac3306f495d28e9af4fde26cc041384ae6fe921755df621dbccca5691235fd17
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 3c649faf68705a6eda2bb2e3a93337b2b30269cd91f48b629dd704f9e950b9f882f63b69ff57287e32c540be78b8c35d7714f3f763c7983e95675790106bc014
|
|
7
|
+
data.tar.gz: 4e8aa3fc7a92b88f83085f950438a3cae213c8d41c454b19f947b67531ba3e21f75cc3f3a1b8ea49a139e594272c60b0f7c943b774a2d4bb62765f77db204e69
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 PrivateCaptcha
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# private-captcha-ruby
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
Ruby client for server-side verification of Private Captcha solutions.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Add this line to your application's Gemfile:
|
|
10
|
+
|
|
11
|
+
```ruby
|
|
12
|
+
gem 'private_captcha'
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
And then execute:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bundle install
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Or install it yourself as:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
gem install private_captcha
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
```ruby
|
|
30
|
+
require 'private_captcha'
|
|
31
|
+
|
|
32
|
+
# Initialize the client with your API key
|
|
33
|
+
client = PrivateCaptcha::Client.new do |config|
|
|
34
|
+
config.api_key = 'your-api-key-here'
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Verify a captcha solution
|
|
38
|
+
begin
|
|
39
|
+
result = client.verify('user-solution-from-frontend')
|
|
40
|
+
if result.success
|
|
41
|
+
puts 'Captcha verified successfully!'
|
|
42
|
+
else
|
|
43
|
+
puts "Verification failed: #{result.error_message}"
|
|
44
|
+
end
|
|
45
|
+
rescue PrivateCaptcha::Error => e
|
|
46
|
+
puts "Error: #{e.message}"
|
|
47
|
+
end
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Usage
|
|
51
|
+
|
|
52
|
+
### Web Framework Integration
|
|
53
|
+
|
|
54
|
+
#### Sinatra Example
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
require 'sinatra'
|
|
58
|
+
require 'private_captcha'
|
|
59
|
+
|
|
60
|
+
client = PrivateCaptcha::Client.new do |config|
|
|
61
|
+
config.api_key = 'your-api-key'
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
post '/submit' do
|
|
65
|
+
begin
|
|
66
|
+
# Verify captcha from form data
|
|
67
|
+
client.verify_request(request)
|
|
68
|
+
|
|
69
|
+
# Process your form data here
|
|
70
|
+
'Form submitted successfully!'
|
|
71
|
+
rescue PrivateCaptcha::Error
|
|
72
|
+
status 400
|
|
73
|
+
'Captcha verification failed'
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
#### Rails Example
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
class FormsController < ApplicationController
|
|
82
|
+
def submit
|
|
83
|
+
client = PrivateCaptcha::Client.new do |config|
|
|
84
|
+
config.api_key = 'your-api-key'
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
begin
|
|
88
|
+
client.verify_request(request)
|
|
89
|
+
# Process form data
|
|
90
|
+
render plain: 'Success!'
|
|
91
|
+
rescue PrivateCaptcha::Error
|
|
92
|
+
render plain: 'Captcha failed', status: :bad_request
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
#### Rack Middleware
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
require 'private_captcha'
|
|
102
|
+
|
|
103
|
+
use PrivateCaptcha::Middleware,
|
|
104
|
+
api_key: 'your-api-key',
|
|
105
|
+
failed_status_code: 403
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Configuration
|
|
109
|
+
|
|
110
|
+
### Client Options
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
require 'private_captcha'
|
|
114
|
+
|
|
115
|
+
client = PrivateCaptcha::Client.new do |config|
|
|
116
|
+
config.api_key = 'your-api-key'
|
|
117
|
+
config.domain = PrivateCaptcha::Configuration::EU_DOMAIN # replace domain for self-hosting or EU isolation
|
|
118
|
+
config.form_field = 'private-captcha-solution' # custom form field name
|
|
119
|
+
config.max_backoff_seconds = 20 # maximum wait between retries
|
|
120
|
+
config.attempts = 5 # number of retry attempts
|
|
121
|
+
config.logger = Logger.new(STDOUT) # optional logger
|
|
122
|
+
end
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Non-standard backend domains
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
require 'private_captcha'
|
|
129
|
+
|
|
130
|
+
# Use EU domain
|
|
131
|
+
eu_client = PrivateCaptcha::Client.new do |config|
|
|
132
|
+
config.api_key = 'your-api-key'
|
|
133
|
+
config.domain = PrivateCaptcha::Configuration::EU_DOMAIN # api.eu.privatecaptcha.com
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Or specify custom domain in case of self-hosting
|
|
137
|
+
custom_client = PrivateCaptcha::Client.new do |config|
|
|
138
|
+
config.api_key = 'your-api-key'
|
|
139
|
+
config.domain = 'your-custom-domain.com'
|
|
140
|
+
end
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Retry Configuration
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
result = client.verify(
|
|
147
|
+
'solution',
|
|
148
|
+
max_backoff_seconds: 15, # maximum wait between retries
|
|
149
|
+
attempts: 3 # number of retry attempts
|
|
150
|
+
)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Requirements
|
|
154
|
+
|
|
155
|
+
- Ruby 3.0+
|
|
156
|
+
- No external dependencies (uses only standard library)
|
|
157
|
+
|
|
158
|
+
## License
|
|
159
|
+
|
|
160
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
|
161
|
+
|
|
162
|
+
## Support
|
|
163
|
+
|
|
164
|
+
For issues with this Ruby client, please open an issue on GitHub.
|
|
165
|
+
For Private Captcha service questions, visit [privatecaptcha.com](https://privatecaptcha.com).
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'uri'
|
|
6
|
+
require 'logger'
|
|
7
|
+
require 'timeout'
|
|
8
|
+
|
|
9
|
+
module PrivateCaptcha
|
|
10
|
+
# Client is the main class for verifying Private Captcha solutions
|
|
11
|
+
class Client # rubocop:disable Metrics/ClassLength
|
|
12
|
+
MIN_BACKOFF_MILLIS = 500
|
|
13
|
+
|
|
14
|
+
attr_reader :config
|
|
15
|
+
|
|
16
|
+
def initialize
|
|
17
|
+
@config = Configuration.new
|
|
18
|
+
yield(@config) if block_given?
|
|
19
|
+
|
|
20
|
+
raise EmptyAPIKeyError if @config.api_key.nil? || @config.api_key.empty?
|
|
21
|
+
|
|
22
|
+
@config.domain = normalize_domain(@config.domain)
|
|
23
|
+
@endpoint = URI("https://#{@config.domain}/verify")
|
|
24
|
+
@logger = @config.logger || Logger.new(IO::NULL)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
28
|
+
def verify(solution, max_backoff_seconds: nil, attempts: nil)
|
|
29
|
+
raise EmptySolutionError if solution.nil? || solution.empty?
|
|
30
|
+
|
|
31
|
+
max_backoff = max_backoff_seconds || @config.max_backoff_seconds
|
|
32
|
+
max_attempts = attempts || @config.attempts
|
|
33
|
+
|
|
34
|
+
@logger.debug('About to start verifying solution') do
|
|
35
|
+
"maxAttempts=#{max_attempts} maxBackoff=#{max_backoff} solution_length=#{solution.length}"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
response = nil
|
|
39
|
+
error = nil
|
|
40
|
+
attempt = 0
|
|
41
|
+
|
|
42
|
+
max_attempts.times do |i|
|
|
43
|
+
attempt = i + 1
|
|
44
|
+
|
|
45
|
+
if i.positive?
|
|
46
|
+
backoff_duration = calculate_backoff(i, max_backoff, error)
|
|
47
|
+
@logger.debug('Failed to send verify request') do
|
|
48
|
+
"attempt=#{attempt} backoff=#{backoff_duration}s error=#{error&.message}"
|
|
49
|
+
end
|
|
50
|
+
sleep(backoff_duration)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
begin
|
|
54
|
+
response = do_verify(solution)
|
|
55
|
+
error = nil
|
|
56
|
+
break
|
|
57
|
+
rescue RetriableError => e
|
|
58
|
+
error = e.original_error
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
@logger.debug('Finished verifying solution') do
|
|
63
|
+
"attempts=#{attempt} success=#{error.nil?}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
if error
|
|
67
|
+
@logger.error("Failed to verify solution after #{attempt} attempts")
|
|
68
|
+
raise VerificationFailedError.new("Failed to verify solution after #{attempt} attempts", attempt)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
response
|
|
72
|
+
end
|
|
73
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
74
|
+
|
|
75
|
+
def verify_request(request, form_field: nil)
|
|
76
|
+
field = form_field || @config.form_field
|
|
77
|
+
solution = extract_form_value(request, field)
|
|
78
|
+
|
|
79
|
+
output = verify(solution)
|
|
80
|
+
|
|
81
|
+
raise Error, "captcha verification failed: #{output.error_message}" unless output.success
|
|
82
|
+
|
|
83
|
+
output
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
def normalize_domain(domain)
|
|
89
|
+
return Configuration::GLOBAL_DOMAIN if domain.nil? || domain.empty?
|
|
90
|
+
|
|
91
|
+
domain = domain.delete_prefix('https://')
|
|
92
|
+
domain = domain.delete_prefix('http://')
|
|
93
|
+
domain.delete_suffix('/')
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
97
|
+
def do_verify(solution)
|
|
98
|
+
request = Net::HTTP::Post.new(@endpoint)
|
|
99
|
+
request['X-Api-Key'] = @config.api_key
|
|
100
|
+
request['User-Agent'] = "private-captcha-ruby/#{VERSION}"
|
|
101
|
+
request['Content-Type'] = 'text/plain'
|
|
102
|
+
request.body = solution
|
|
103
|
+
|
|
104
|
+
@logger.debug('Sending HTTP request') { "path=#{@endpoint.path} method=POST" }
|
|
105
|
+
|
|
106
|
+
response = nil
|
|
107
|
+
begin
|
|
108
|
+
response = Net::HTTP.start(@endpoint.hostname, @endpoint.port, use_ssl: true) do |http|
|
|
109
|
+
http.request(request)
|
|
110
|
+
end
|
|
111
|
+
rescue SocketError, IOError, Timeout::Error, SystemCallError => e
|
|
112
|
+
@logger.debug('Failed to send HTTP request') { "error=#{e.message}" }
|
|
113
|
+
raise RetriableError, e
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
@logger.debug('HTTP request finished') do
|
|
117
|
+
"path=#{@endpoint.path} status=#{response.code}"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
status_code = response.code.to_i
|
|
121
|
+
request_id = response['X-Trace-ID']
|
|
122
|
+
|
|
123
|
+
case status_code
|
|
124
|
+
when 429
|
|
125
|
+
retry_after = parse_retry_after(response['Retry-After'])
|
|
126
|
+
@logger.debug('Rate limited') do
|
|
127
|
+
"retryAfter=#{retry_after} rateLimit=#{response['X-RateLimit-Limit']}"
|
|
128
|
+
end
|
|
129
|
+
raise RetriableError, HTTPError.new(status_code, retry_after)
|
|
130
|
+
when 500, 502, 503, 504, 408, 425
|
|
131
|
+
raise RetriableError, HTTPError.new(status_code)
|
|
132
|
+
when 300..599
|
|
133
|
+
raise HTTPError, status_code
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
begin
|
|
137
|
+
json_data = JSON.parse(response.body)
|
|
138
|
+
VerifyOutput.from_json(json_data, request_id: request_id)
|
|
139
|
+
rescue JSON::ParserError => e
|
|
140
|
+
@logger.debug('Failed to parse response') { "error=#{e.message}" }
|
|
141
|
+
raise RetriableError, e
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
145
|
+
|
|
146
|
+
def parse_retry_after(header)
|
|
147
|
+
return nil if header.nil? || header.empty?
|
|
148
|
+
|
|
149
|
+
Integer(header)
|
|
150
|
+
rescue ArgumentError
|
|
151
|
+
nil
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def calculate_backoff(attempt, max_backoff, error)
|
|
155
|
+
backoff = (MIN_BACKOFF_MILLIS / 1000.0) * (2**attempt)
|
|
156
|
+
|
|
157
|
+
backoff = [backoff, error.seconds].max if error.is_a?(HTTPError) && error.seconds
|
|
158
|
+
|
|
159
|
+
[backoff, max_backoff].min
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def extract_form_value(request, field)
|
|
163
|
+
# Support for Rack::Request
|
|
164
|
+
if request.respond_to?(:params)
|
|
165
|
+
request.params[field]
|
|
166
|
+
# Support for Rails ActionDispatch::Request
|
|
167
|
+
elsif request.respond_to?(:[])
|
|
168
|
+
request[field]
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PrivateCaptcha
|
|
4
|
+
# Configuration holds the settings for the Private Captcha client
|
|
5
|
+
class Configuration
|
|
6
|
+
GLOBAL_DOMAIN = 'api.privatecaptcha.com'
|
|
7
|
+
EU_DOMAIN = 'api.eu.privatecaptcha.com'
|
|
8
|
+
DEFAULT_FORM_FIELD = 'private-captcha-solution'
|
|
9
|
+
DEFAULT_FAILED_STATUS_CODE = 403
|
|
10
|
+
|
|
11
|
+
attr_accessor :domain, :api_key, :form_field, :failed_status_code,
|
|
12
|
+
:max_backoff_seconds, :attempts, :logger
|
|
13
|
+
|
|
14
|
+
def initialize
|
|
15
|
+
@domain = GLOBAL_DOMAIN
|
|
16
|
+
@api_key = nil
|
|
17
|
+
@form_field = DEFAULT_FORM_FIELD
|
|
18
|
+
@failed_status_code = DEFAULT_FAILED_STATUS_CODE
|
|
19
|
+
@max_backoff_seconds = 20
|
|
20
|
+
@attempts = 5
|
|
21
|
+
@logger = nil
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PrivateCaptcha
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
# EmptyAPIKeyError is raised when the API key is not provided or is empty
|
|
7
|
+
class EmptyAPIKeyError < Error
|
|
8
|
+
def initialize(msg = 'API key is empty')
|
|
9
|
+
super
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# EmptySolutionError is raised when the solution is not provided or is empty
|
|
14
|
+
class EmptySolutionError < Error
|
|
15
|
+
def initialize(msg = 'solution is empty')
|
|
16
|
+
super
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# HTTPError is raised when an HTTP error occurs during verification
|
|
21
|
+
class HTTPError < Error
|
|
22
|
+
attr_reader :status_code, :seconds
|
|
23
|
+
|
|
24
|
+
def initialize(status_code, seconds = nil)
|
|
25
|
+
@status_code = status_code
|
|
26
|
+
@seconds = seconds
|
|
27
|
+
super("HTTP error #{status_code}")
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# RetriableError wraps errors that can be retried
|
|
32
|
+
class RetriableError < Error
|
|
33
|
+
attr_reader :original_error
|
|
34
|
+
|
|
35
|
+
def initialize(error)
|
|
36
|
+
@original_error = error
|
|
37
|
+
super(error.message)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# VerificationFailedError is raised when verification fails after all retry attempts
|
|
42
|
+
class VerificationFailedError < Error
|
|
43
|
+
attr_reader :attempts
|
|
44
|
+
|
|
45
|
+
def initialize(message, attempts)
|
|
46
|
+
@attempts = attempts
|
|
47
|
+
super(message)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rack'
|
|
4
|
+
|
|
5
|
+
module PrivateCaptcha
|
|
6
|
+
# Middleware provides Rack middleware for automatic captcha verification
|
|
7
|
+
class Middleware
|
|
8
|
+
# rubocop:disable Metrics/AbcSize
|
|
9
|
+
def initialize(app, api_key:, **options)
|
|
10
|
+
@app = app
|
|
11
|
+
@client = Client.new do |config|
|
|
12
|
+
config.api_key = api_key
|
|
13
|
+
config.domain = options[:domain] if options[:domain]
|
|
14
|
+
config.form_field = options[:form_field] if options[:form_field]
|
|
15
|
+
config.failed_status_code = options[:failed_status_code] if options[:failed_status_code]
|
|
16
|
+
config.max_backoff_seconds = options[:max_backoff_seconds] if options[:max_backoff_seconds]
|
|
17
|
+
config.attempts = options[:attempts] if options[:attempts]
|
|
18
|
+
config.logger = options[:logger] if options[:logger]
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
# rubocop:enable Metrics/AbcSize
|
|
22
|
+
|
|
23
|
+
# rubocop:disable Metrics/MethodLength
|
|
24
|
+
def call(env)
|
|
25
|
+
request = Rack::Request.new(env)
|
|
26
|
+
|
|
27
|
+
begin
|
|
28
|
+
@client.verify_request(request)
|
|
29
|
+
rescue Error
|
|
30
|
+
return [
|
|
31
|
+
@client.config.failed_status_code,
|
|
32
|
+
{ 'Content-Type' => 'text/plain' },
|
|
33
|
+
[Rack::Utils::HTTP_STATUS_CODES[@client.config.failed_status_code]]
|
|
34
|
+
]
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
@app.call(env)
|
|
38
|
+
end
|
|
39
|
+
# rubocop:enable Metrics/MethodLength
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PrivateCaptcha
|
|
4
|
+
# VerifyOutput represents the result of a captcha verification
|
|
5
|
+
class VerifyOutput
|
|
6
|
+
VERIFY_NO_ERROR = 0
|
|
7
|
+
VERIFY_ERROR_OTHER = 1
|
|
8
|
+
DUPLICATE_SOLUTIONS_ERROR = 2
|
|
9
|
+
INVALID_SOLUTION_ERROR = 3
|
|
10
|
+
PARSE_RESPONSE_ERROR = 4
|
|
11
|
+
PUZZLE_EXPIRED_ERROR = 5
|
|
12
|
+
INVALID_PROPERTY_ERROR = 6
|
|
13
|
+
WRONG_OWNER_ERROR = 7
|
|
14
|
+
VERIFIED_BEFORE_ERROR = 8
|
|
15
|
+
MAINTENANCE_MODE_ERROR = 9
|
|
16
|
+
TEST_PROPERTY_ERROR = 10
|
|
17
|
+
INTEGRITY_ERROR = 11
|
|
18
|
+
VERIFY_CODES_COUNT = 12
|
|
19
|
+
|
|
20
|
+
ERROR_MESSAGES = {
|
|
21
|
+
VERIFY_NO_ERROR => '',
|
|
22
|
+
VERIFY_ERROR_OTHER => 'error-other',
|
|
23
|
+
DUPLICATE_SOLUTIONS_ERROR => 'solution-duplicates',
|
|
24
|
+
INVALID_SOLUTION_ERROR => 'solution-invalid',
|
|
25
|
+
PARSE_RESPONSE_ERROR => 'solution-bad-format',
|
|
26
|
+
PUZZLE_EXPIRED_ERROR => 'puzzle-expired',
|
|
27
|
+
INVALID_PROPERTY_ERROR => 'property-invalid',
|
|
28
|
+
WRONG_OWNER_ERROR => 'property-owner-mismatch',
|
|
29
|
+
VERIFIED_BEFORE_ERROR => 'solution-verified-before',
|
|
30
|
+
MAINTENANCE_MODE_ERROR => 'maintenance-mode',
|
|
31
|
+
TEST_PROPERTY_ERROR => 'property-test',
|
|
32
|
+
INTEGRITY_ERROR => 'integrity-error'
|
|
33
|
+
}.freeze
|
|
34
|
+
|
|
35
|
+
attr_accessor :success, :code, :origin, :timestamp
|
|
36
|
+
attr_reader :request_id, :attempt
|
|
37
|
+
|
|
38
|
+
def initialize(success: false, code: VERIFY_NO_ERROR, origin: nil, timestamp: nil, request_id: nil, attempt: 0)
|
|
39
|
+
@success = success
|
|
40
|
+
@code = code
|
|
41
|
+
@origin = origin
|
|
42
|
+
@timestamp = timestamp
|
|
43
|
+
@request_id = request_id
|
|
44
|
+
@attempt = attempt
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def error_message
|
|
48
|
+
ERROR_MESSAGES.fetch(@code, 'error')
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.from_json(json_data, request_id: nil, attempt: 0)
|
|
52
|
+
new(
|
|
53
|
+
success: json_data['success'],
|
|
54
|
+
code: json_data['code'] || VERIFY_NO_ERROR,
|
|
55
|
+
origin: json_data['origin'],
|
|
56
|
+
timestamp: json_data['timestamp'],
|
|
57
|
+
request_id: request_id,
|
|
58
|
+
attempt: attempt
|
|
59
|
+
)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'private_captcha/version'
|
|
4
|
+
require_relative 'private_captcha/errors'
|
|
5
|
+
require_relative 'private_captcha/verify_output'
|
|
6
|
+
require_relative 'private_captcha/configuration'
|
|
7
|
+
require_relative 'private_captcha/client'
|
|
8
|
+
require_relative 'private_captcha/middleware'
|
|
9
|
+
|
|
10
|
+
# PrivateCaptcha is a Ruby client library for integrating Private Captcha
|
|
11
|
+
# verification into your applications. It provides a simple API for verifying
|
|
12
|
+
# captcha solutions and can be used as Rack middleware.
|
|
13
|
+
module PrivateCaptcha
|
|
14
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: private_captcha
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Taras Kushnir
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2025-10-26 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rack
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '2.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '2.0'
|
|
27
|
+
description: A Ruby library for integrating Private Captcha verification into your
|
|
28
|
+
applications
|
|
29
|
+
email:
|
|
30
|
+
executables: []
|
|
31
|
+
extensions: []
|
|
32
|
+
extra_rdoc_files: []
|
|
33
|
+
files:
|
|
34
|
+
- LICENSE
|
|
35
|
+
- README.md
|
|
36
|
+
- lib/private_captcha.rb
|
|
37
|
+
- lib/private_captcha/client.rb
|
|
38
|
+
- lib/private_captcha/configuration.rb
|
|
39
|
+
- lib/private_captcha/errors.rb
|
|
40
|
+
- lib/private_captcha/middleware.rb
|
|
41
|
+
- lib/private_captcha/verify_output.rb
|
|
42
|
+
- lib/private_captcha/version.rb
|
|
43
|
+
homepage: https://privatecaptcha.com
|
|
44
|
+
licenses:
|
|
45
|
+
- MIT
|
|
46
|
+
metadata:
|
|
47
|
+
homepage_uri: https://privatecaptcha.com
|
|
48
|
+
source_code_uri: https://github.com/PrivateCaptcha/private-captcha-ruby
|
|
49
|
+
rubygems_mfa_required: 'true'
|
|
50
|
+
post_install_message:
|
|
51
|
+
rdoc_options: []
|
|
52
|
+
require_paths:
|
|
53
|
+
- lib
|
|
54
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
55
|
+
requirements:
|
|
56
|
+
- - ">="
|
|
57
|
+
- !ruby/object:Gem::Version
|
|
58
|
+
version: 2.7.0
|
|
59
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
60
|
+
requirements:
|
|
61
|
+
- - ">="
|
|
62
|
+
- !ruby/object:Gem::Version
|
|
63
|
+
version: '0'
|
|
64
|
+
requirements: []
|
|
65
|
+
rubygems_version: 3.4.20
|
|
66
|
+
signing_key:
|
|
67
|
+
specification_version: 4
|
|
68
|
+
summary: Ruby client for server-side Private Captcha API
|
|
69
|
+
test_files: []
|