eu_captcha 1.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.
- checksums.yaml +7 -0
- data/LICENSE +25 -0
- data/README.md +192 -0
- data/lib/eu_captcha/client.rb +190 -0
- data/lib/eu_captcha/error.rb +30 -0
- data/lib/eu_captcha/result.rb +99 -0
- data/lib/eu_captcha/version.rb +29 -0
- data/lib/eu_captcha.rb +30 -0
- metadata +74 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: ddd0db23dcc5a6704ea068e01f4f35e041cf45411025d8ec42a02fb13e737ef9
|
|
4
|
+
data.tar.gz: d06e2d715767ba0f4f124103552a1d7e09caeb4de5fbe0beae54699203d959dc
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 2c41cde2d9f0650caeabe08e722c3a6a50f5c5d2da8ef473fd133a5a0de47ef000ec8a369128972e09332b46d3b07e432fb66fdfee4696cc271cd48bb25cd02f
|
|
7
|
+
data.tar.gz: 83a76af0123ec9d64a6a90a7ba88f54b7becb1127fdf92f3969345445a2ba72c8107f8662cf4c58ca879f3e53206ca782a6c74c8521bf628738901fdbe9e9760
|
data/LICENSE
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
BSD 2-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright 2026 Myra Security GmbH
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without modification,
|
|
6
|
+
are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice,
|
|
9
|
+
this list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
16
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
17
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
18
|
+
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
19
|
+
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
20
|
+
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
21
|
+
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
22
|
+
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
23
|
+
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
24
|
+
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
25
|
+
POSSIBILITY OF SUCH DAMAGE.
|
data/README.md
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# EU-Captcha Ruby client
|
|
2
|
+
|
|
3
|
+
Privacy-first, no-cookie, no-manual-interaction bot protection for Ruby applications. Automatically filters bots, spam, and credential-stuffing attempts without requiring any user interaction.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Ruby 2.7 or later
|
|
8
|
+
- No runtime dependencies (uses Ruby's built-in `net/http`)
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
gem install eu_captcha
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Or add it to your `Gemfile`:
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
gem 'eu_captcha'
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Getting credentials
|
|
23
|
+
|
|
24
|
+
1. Register at [app.eu-captcha.eu](https://app.eu-captcha.eu/user-registration)
|
|
25
|
+
2. Create a site and copy the **sitekey** and **secret** from the dashboard
|
|
26
|
+
|
|
27
|
+
## Quick start
|
|
28
|
+
|
|
29
|
+
> **Using a SPA framework?** The script tag and `<div>` approach below is for server-rendered pages.
|
|
30
|
+
> If you are building with React, Vue, or Angular, use the matching npm package for the frontend widget
|
|
31
|
+
> and continue to use this gem for server-side verification only.
|
|
32
|
+
> See [SPA integration guides](https://docs.eu-captcha.eu/integration/spa/) for details.
|
|
33
|
+
|
|
34
|
+
Add the widget script to any page that contains a form you want to protect:
|
|
35
|
+
|
|
36
|
+
```html
|
|
37
|
+
<script src="https://cdn.eu-captcha.eu/verify.js" async defer></script>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Place the widget inside your form:
|
|
41
|
+
|
|
42
|
+
```html
|
|
43
|
+
<div class="eu-captcha" data-sitekey="EUCAPTCHA_SITE_KEY"></div>
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Verify the submitted token on your server:
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
require 'eu_captcha'
|
|
50
|
+
|
|
51
|
+
captcha = EuCaptcha::Client.new(
|
|
52
|
+
sitekey: ENV['EUCAPTCHA_SITE_KEY'],
|
|
53
|
+
secret: ENV['EUCAPTCHA_SECRET_KEY']
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
result = captcha.validate(
|
|
57
|
+
token: params['eu-captcha-response'],
|
|
58
|
+
remote_addr: request.ip
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
render_error unless result.success?
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Configuration options
|
|
65
|
+
|
|
66
|
+
All options are passed as keyword arguments to the constructor.
|
|
67
|
+
|
|
68
|
+
| Option | Type | Default | Description |
|
|
69
|
+
|---------------------|---------|-----------------------|-------------|
|
|
70
|
+
| `sitekey` | String | — | **Required.** Public sitekey from the dashboard. |
|
|
71
|
+
| `secret` | String | — | **Required.** Secret key from the dashboard. Never expose this client-side. |
|
|
72
|
+
| `fail_default` | Boolean | `true` | Return value used for both network and token state when the API cannot be reached. `true` = fail open (allow on error); `false` = fail closed (deny on error). |
|
|
73
|
+
| `check_cdn_headers` | Boolean | `true` | When `true`, the client IP is resolved from CDN/proxy headers (`HTTP_CLIENT_IP`, `HTTP_X_FORWARDED_FOR`, `HTTP_X_REAL_IP`) in the Rack environment before falling back to `REMOTE_ADDR`. Set to `false` when not behind a proxy, or when you pass the IP explicitly. |
|
|
74
|
+
| `verify_url` | String | *(production URL)* | Override the EU-Captcha verify endpoint. Useful for testing. |
|
|
75
|
+
| `credentials_url` | String | *(production URL)* | Override the EU-Captcha verify-credentials endpoint. |
|
|
76
|
+
|
|
77
|
+
## The result object
|
|
78
|
+
|
|
79
|
+
`validate` returns an `EuCaptcha::Result` with these methods:
|
|
80
|
+
|
|
81
|
+
| Method | Returns `true` when… |
|
|
82
|
+
|---------------------|--------------------------------------------------------------|
|
|
83
|
+
| `success?` | The API was reached **and** the token is valid. |
|
|
84
|
+
| `success_network?` | The API call completed without a network or transport error. |
|
|
85
|
+
| `success_token?` | The API reported the submitted token as valid. |
|
|
86
|
+
| `train` | Returns `true`/`false`/`nil` (see below). |
|
|
87
|
+
|
|
88
|
+
Checking both states separately lets you distinguish a user failing the captcha from an API outage:
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
result = captcha.validate(token: params['eu-captcha-response'], remote_addr: request.ip)
|
|
92
|
+
|
|
93
|
+
unless result.success_network?
|
|
94
|
+
# Could not reach the API — consider logging or alerting
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
unless result.success_token?
|
|
98
|
+
# Token was rejected — the submission is likely automated
|
|
99
|
+
end
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The `train` method returns `true` when the API skipped real validation and forced success — typically because the sitekey does not exist, the secret does not match, or the sitekey's protection toggle is disabled. `success?` already accounts for this flag and returns `false` in that case. Returns `nil` on network failure.
|
|
103
|
+
|
|
104
|
+
## Verifying credentials
|
|
105
|
+
|
|
106
|
+
Use `verify_credentials` to confirm your sitekey and secret are valid without submitting a client token. Useful for startup or configuration checks:
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
captcha = EuCaptcha::Client.new(
|
|
110
|
+
sitekey: ENV['EUCAPTCHA_SITE_KEY'],
|
|
111
|
+
secret: ENV['EUCAPTCHA_SECRET_KEY']
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
unless captcha.verify_credentials
|
|
115
|
+
# Credentials are invalid or the API is unreachable — log and alert
|
|
116
|
+
end
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
`verify_credentials` returns `false` on any network or API error rather than raising, so it is safe to call during application initialisation.
|
|
120
|
+
|
|
121
|
+
## Rails
|
|
122
|
+
|
|
123
|
+
Store credentials in `config/credentials.yml.enc` (or `.env` + dotenv) and create a client in an initializer:
|
|
124
|
+
|
|
125
|
+
**`config/initializers/eu_captcha.rb`**
|
|
126
|
+
|
|
127
|
+
```ruby
|
|
128
|
+
EU_CAPTCHA = EuCaptcha::Client.new(
|
|
129
|
+
sitekey: Rails.application.credentials.dig(:eucaptcha, :sitekey),
|
|
130
|
+
secret: Rails.application.credentials.dig(:eucaptcha, :secret)
|
|
131
|
+
)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**Controller**
|
|
135
|
+
|
|
136
|
+
```ruby
|
|
137
|
+
class ContactController < ApplicationController
|
|
138
|
+
def create
|
|
139
|
+
result = EU_CAPTCHA.validate(
|
|
140
|
+
token: params['eu-captcha-response'],
|
|
141
|
+
remote_addr: request.ip,
|
|
142
|
+
user_agent: request.user_agent
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
unless result.success?
|
|
146
|
+
render json: { error: 'CAPTCHA verification failed' }, status: :unprocessable_entity
|
|
147
|
+
return
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# process the form...
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`request.ip` respects Rails' trusted-proxy configuration, so the real visitor IP is forwarded correctly when running behind a CDN or load balancer.
|
|
156
|
+
|
|
157
|
+
## Sinatra / Rack
|
|
158
|
+
|
|
159
|
+
Pass the Rack environment directly and let the library resolve the client IP automatically:
|
|
160
|
+
|
|
161
|
+
```ruby
|
|
162
|
+
require 'sinatra'
|
|
163
|
+
require 'eu_captcha'
|
|
164
|
+
|
|
165
|
+
CAPTCHA = EuCaptcha::Client.new(
|
|
166
|
+
sitekey: ENV['EUCAPTCHA_SITE_KEY'],
|
|
167
|
+
secret: ENV['EUCAPTCHA_SECRET_KEY']
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
post '/contact' do
|
|
171
|
+
result = CAPTCHA.validate(
|
|
172
|
+
token: params['eu-captcha-response'],
|
|
173
|
+
rack_env: env
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
halt 422, 'CAPTCHA verification failed' unless result.success?
|
|
177
|
+
|
|
178
|
+
# process the form...
|
|
179
|
+
end
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
When `rack_env` is provided, `validate` automatically resolves the client IP and User-Agent from the Rack environment, respecting the `check_cdn_headers` setting.
|
|
183
|
+
|
|
184
|
+
## Further reading
|
|
185
|
+
|
|
186
|
+
- [Full documentation](https://docs.eu-captcha.eu)
|
|
187
|
+
- [Server-side verification reference](https://docs.eu-captcha.eu/integration/server-side-verification/)
|
|
188
|
+
- [SPA integration guides](https://docs.eu-captcha.eu/integration/spa/) (React / Next.js, Vue / Nuxt, Angular)
|
|
189
|
+
|
|
190
|
+
## License
|
|
191
|
+
|
|
192
|
+
BSD 2-Clause. See [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright 2026 Myra Security GmbH
|
|
4
|
+
#
|
|
5
|
+
# Redistribution and use in source and binary forms, with or without modification,
|
|
6
|
+
# are permitted provided that the following conditions are met:
|
|
7
|
+
#
|
|
8
|
+
# 1. Redistributions of source code must retain the above copyright notice,
|
|
9
|
+
# this list of conditions and the following disclaimer.
|
|
10
|
+
#
|
|
11
|
+
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
# this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
# and/or other materials provided with the distribution.
|
|
14
|
+
#
|
|
15
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
16
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
17
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
18
|
+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
19
|
+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
20
|
+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
21
|
+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
22
|
+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
23
|
+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
24
|
+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
25
|
+
# POSSIBILITY OF SUCH DAMAGE.
|
|
26
|
+
|
|
27
|
+
require 'net/http'
|
|
28
|
+
require 'json'
|
|
29
|
+
require 'uri'
|
|
30
|
+
require 'ipaddr'
|
|
31
|
+
|
|
32
|
+
module EuCaptcha
|
|
33
|
+
# Server-side EU-Captcha verification client.
|
|
34
|
+
#
|
|
35
|
+
# Instantiate once per application (e.g. as a singleton or dependency-injected
|
|
36
|
+
# service) and call {#validate} on every form submission you want to protect.
|
|
37
|
+
#
|
|
38
|
+
# @example Quick start
|
|
39
|
+
# captcha = EuCaptcha::Client.new(sitekey: ENV['EUCAPTCHA_SITE_KEY'],
|
|
40
|
+
# secret: ENV['EUCAPTCHA_SECRET_KEY'])
|
|
41
|
+
#
|
|
42
|
+
# result = captcha.validate(token: params['eu-captcha-response'],
|
|
43
|
+
# remote_addr: request.ip)
|
|
44
|
+
#
|
|
45
|
+
# render_error unless result.success?
|
|
46
|
+
class Client
|
|
47
|
+
VERIFY_URL = 'https://api.eu-captcha.eu/v1/verify/'
|
|
48
|
+
CREDENTIALS_URL = 'https://api.eu-captcha.eu/v1/verify-credentials'
|
|
49
|
+
|
|
50
|
+
# @param sitekey [String] The public site key used to identify your site on the
|
|
51
|
+
# client side.
|
|
52
|
+
# @param secret [String] The private secret key used to authenticate server-side
|
|
53
|
+
# verification requests. Never expose this client-side.
|
|
54
|
+
# @param verify_url [String] The EU-Captcha verify endpoint. Defaults to the
|
|
55
|
+
# production URL.
|
|
56
|
+
# @param credentials_url [String] The EU-Captcha verify-credentials endpoint. Defaults to
|
|
57
|
+
# the production URL.
|
|
58
|
+
# @param fail_default [Boolean] Whether to treat an API communication failure as a
|
|
59
|
+
# successful validation. Defaults to +true+ to avoid blocking legitimate users when the
|
|
60
|
+
# API is unreachable (fail open). Set to +false+ to deny on error (fail closed).
|
|
61
|
+
# @param check_cdn_headers [Boolean] When +true+ (default), the client IP is resolved from
|
|
62
|
+
# CDN/proxy headers (+HTTP_CLIENT_IP+, +HTTP_X_FORWARDED_FOR+, +HTTP_X_REAL_IP+) in the
|
|
63
|
+
# Rack environment before falling back to +REMOTE_ADDR+. Set to +false+ when running
|
|
64
|
+
# behind no proxy, or when you supply +remote_addr+ explicitly on every {#validate} call.
|
|
65
|
+
#
|
|
66
|
+
# @raise [EuCaptcha::Error] if +sitekey+ or +secret+ are empty.
|
|
67
|
+
def initialize(
|
|
68
|
+
sitekey:,
|
|
69
|
+
secret:,
|
|
70
|
+
verify_url: VERIFY_URL,
|
|
71
|
+
credentials_url: CREDENTIALS_URL,
|
|
72
|
+
fail_default: true,
|
|
73
|
+
check_cdn_headers: true
|
|
74
|
+
)
|
|
75
|
+
raise EuCaptcha::Error, 'sitekey and secret are required' if sitekey.to_s.empty? || secret.to_s.empty?
|
|
76
|
+
|
|
77
|
+
@sitekey = sitekey
|
|
78
|
+
@secret = secret
|
|
79
|
+
@verify_url = verify_url
|
|
80
|
+
@credentials_url = credentials_url
|
|
81
|
+
@fail_default = fail_default
|
|
82
|
+
@check_cdn_headers = check_cdn_headers
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Validates a captcha token against the EU-Captcha API.
|
|
86
|
+
#
|
|
87
|
+
# All parameters are optional: pass what you have and let the library fill in the rest
|
|
88
|
+
# from +rack_env+ when provided.
|
|
89
|
+
#
|
|
90
|
+
# On any API or network failure the result reflects +fail_default+ for +state_network+
|
|
91
|
+
# and +state_token+, and +nil+ for +state_train+, so callers can distinguish between a
|
|
92
|
+
# failed validation and a failed network request.
|
|
93
|
+
#
|
|
94
|
+
# @param token [String, nil] The captcha response token submitted by the client.
|
|
95
|
+
# @param remote_addr [String] The client's IP address. When empty, resolved from
|
|
96
|
+
# +rack_env+ (respecting +check_cdn_headers+).
|
|
97
|
+
# @param user_agent [String] The client's User-Agent header. When empty, read from
|
|
98
|
+
# +rack_env['HTTP_USER_AGENT']+.
|
|
99
|
+
# @param rack_env [Hash, nil] A Rack environment hash used for automatic IP and
|
|
100
|
+
# User-Agent resolution when +remote_addr+ or +user_agent+ are not supplied explicitly.
|
|
101
|
+
#
|
|
102
|
+
# @return [EuCaptcha::Result]
|
|
103
|
+
def validate(token: nil, remote_addr: '', user_agent: '', rack_env: nil)
|
|
104
|
+
effective_addr = remote_addr.to_s.empty? ? resolve_client_ip(rack_env) : remote_addr.to_s
|
|
105
|
+
effective_ua = user_agent.to_s.empty? ? rack_user_agent(rack_env) : user_agent.to_s
|
|
106
|
+
|
|
107
|
+
payload = {
|
|
108
|
+
sitekey: @sitekey,
|
|
109
|
+
secret: @secret,
|
|
110
|
+
client_ip: effective_addr,
|
|
111
|
+
client_token: token.to_s,
|
|
112
|
+
client_user_agent: effective_ua
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
body = post_json(@verify_url, payload)
|
|
116
|
+
Result.new(
|
|
117
|
+
state_network: true,
|
|
118
|
+
state_token: body.fetch('success', false) == true,
|
|
119
|
+
state_train: body.fetch('train', false) == true
|
|
120
|
+
)
|
|
121
|
+
rescue StandardError
|
|
122
|
+
Result.new(
|
|
123
|
+
state_network: @fail_default,
|
|
124
|
+
state_token: @fail_default,
|
|
125
|
+
state_train: nil
|
|
126
|
+
)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Checks whether the configured sitekey and secret are valid without requiring a client token.
|
|
130
|
+
#
|
|
131
|
+
# Intended for startup or configuration checks. Returns +false+ on any network or API error
|
|
132
|
+
# rather than raising, so callers can log a warning and continue initialisation.
|
|
133
|
+
#
|
|
134
|
+
# @return [Boolean] +true+ if the API confirms the sitekey/secret pair is valid.
|
|
135
|
+
def verify_credentials
|
|
136
|
+
payload = { sitekey: @sitekey, secret: @secret }
|
|
137
|
+
body = post_json(@credentials_url, payload)
|
|
138
|
+
body.fetch('valid', false) == true
|
|
139
|
+
rescue StandardError
|
|
140
|
+
false
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
private
|
|
144
|
+
|
|
145
|
+
# Resolves the client IP address from a Rack environment hash.
|
|
146
|
+
#
|
|
147
|
+
# When +check_cdn_headers+ is +true+, the following headers are checked in order and the
|
|
148
|
+
# first value that passes IP validation is returned:
|
|
149
|
+
# +HTTP_CLIENT_IP+, +HTTP_X_FORWARDED_FOR+ (first entry), +HTTP_X_REAL_IP+
|
|
150
|
+
# Falls back to +REMOTE_ADDR+ in all cases.
|
|
151
|
+
def resolve_client_ip(rack_env)
|
|
152
|
+
return '' if rack_env.nil?
|
|
153
|
+
|
|
154
|
+
if @check_cdn_headers
|
|
155
|
+
%w[HTTP_CLIENT_IP HTTP_X_FORWARDED_FOR HTTP_X_REAL_IP].each do |header|
|
|
156
|
+
value = rack_env[header].to_s
|
|
157
|
+
next if value.empty?
|
|
158
|
+
|
|
159
|
+
ip = value.split(',').first.to_s.strip
|
|
160
|
+
return ip if valid_ip?(ip)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
rack_env.fetch('REMOTE_ADDR', '')
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def rack_user_agent(rack_env)
|
|
168
|
+
rack_env&.fetch('HTTP_USER_AGENT', '') || ''
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def valid_ip?(ip)
|
|
172
|
+
IPAddr.new(ip)
|
|
173
|
+
true
|
|
174
|
+
rescue IPAddr::InvalidAddressError, IPAddr::AddressFamilyError
|
|
175
|
+
false
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def post_json(url, payload)
|
|
179
|
+
uri = URI(url)
|
|
180
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
|
181
|
+
request = Net::HTTP::Post.new(uri)
|
|
182
|
+
request['Content-Type'] = 'application/json'
|
|
183
|
+
request['Accept'] = 'application/json'
|
|
184
|
+
request.body = JSON.generate(payload)
|
|
185
|
+
response = http.request(request)
|
|
186
|
+
JSON.parse(response.body)
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright 2026 Myra Security GmbH
|
|
4
|
+
#
|
|
5
|
+
# Redistribution and use in source and binary forms, with or without modification,
|
|
6
|
+
# are permitted provided that the following conditions are met:
|
|
7
|
+
#
|
|
8
|
+
# 1. Redistributions of source code must retain the above copyright notice,
|
|
9
|
+
# this list of conditions and the following disclaimer.
|
|
10
|
+
#
|
|
11
|
+
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
# this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
# and/or other materials provided with the distribution.
|
|
14
|
+
#
|
|
15
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
16
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
17
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
18
|
+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
19
|
+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
20
|
+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
21
|
+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
22
|
+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
23
|
+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
24
|
+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
25
|
+
# POSSIBILITY OF SUCH DAMAGE.
|
|
26
|
+
|
|
27
|
+
module EuCaptcha
|
|
28
|
+
# Raised when the client is misconfigured, for example when +sitekey+ or +secret+ are empty.
|
|
29
|
+
class Error < StandardError; end
|
|
30
|
+
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright 2026 Myra Security GmbH
|
|
4
|
+
#
|
|
5
|
+
# Redistribution and use in source and binary forms, with or without modification,
|
|
6
|
+
# are permitted provided that the following conditions are met:
|
|
7
|
+
#
|
|
8
|
+
# 1. Redistributions of source code must retain the above copyright notice,
|
|
9
|
+
# this list of conditions and the following disclaimer.
|
|
10
|
+
#
|
|
11
|
+
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
# this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
# and/or other materials provided with the distribution.
|
|
14
|
+
#
|
|
15
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
16
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
17
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
18
|
+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
19
|
+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
20
|
+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
21
|
+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
22
|
+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
23
|
+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
24
|
+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
25
|
+
# POSSIBILITY OF SUCH DAMAGE.
|
|
26
|
+
|
|
27
|
+
module EuCaptcha
|
|
28
|
+
# Holds the outcome of a single {Client#validate} call.
|
|
29
|
+
#
|
|
30
|
+
# The three state values map directly to the API response fields:
|
|
31
|
+
#
|
|
32
|
+
# * +state_network+ – whether the HTTP request completed without a transport error.
|
|
33
|
+
# * +state_token+ – whether the API reported the submitted token as valid
|
|
34
|
+
# (+success+ field in the JSON response).
|
|
35
|
+
# * +state_train+ – the +train+ flag from the API response (+true+/+false+),
|
|
36
|
+
# or +nil+ when no response was received (network failure).
|
|
37
|
+
class Result
|
|
38
|
+
# @param state_network [Boolean] Whether the API request completed successfully.
|
|
39
|
+
# @param state_token [Boolean] Whether the captcha token was accepted as valid by the API.
|
|
40
|
+
# @param state_train [Boolean, nil] The +train+ flag from the API response. +true+ means the
|
|
41
|
+
# API skipped real validation and forced success (misconfigured credentials or disabled
|
|
42
|
+
# protection). +false+ means normal operation. +nil+ when no API response was received
|
|
43
|
+
# (network failure).
|
|
44
|
+
def initialize(state_network:, state_token:, state_train: nil)
|
|
45
|
+
@state_network = state_network
|
|
46
|
+
@state_token = state_token
|
|
47
|
+
@state_train = state_train
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Returns +true+ only if the API request succeeded, the token was valid,
|
|
51
|
+
# and the +train+ flag is not set.
|
|
52
|
+
#
|
|
53
|
+
# When +train+ is +true+ the API forced +success+ to +true+ without performing
|
|
54
|
+
# real validation (misconfigured credentials or disabled protection). This
|
|
55
|
+
# method treats that case as a failure so misconfigured sites fail securely
|
|
56
|
+
# by default. Use {#train} to inspect the flag directly.
|
|
57
|
+
#
|
|
58
|
+
# @return [Boolean]
|
|
59
|
+
def success?
|
|
60
|
+
@state_network && @state_token && !@state_train
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Returns +true+ if the API request completed without a network or HTTP error.
|
|
64
|
+
#
|
|
65
|
+
# @return [Boolean]
|
|
66
|
+
def success_network?
|
|
67
|
+
@state_network
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Returns +true+ if the API confirmed the captcha token as valid.
|
|
71
|
+
#
|
|
72
|
+
# Note: this reflects the raw +success+ field from the API response. When
|
|
73
|
+
# {#train} returns +true+, the API forced this field to +true+ without real
|
|
74
|
+
# validation. Prefer {#success?} for the safe combined check.
|
|
75
|
+
#
|
|
76
|
+
# @return [Boolean]
|
|
77
|
+
def success_token?
|
|
78
|
+
@state_token
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Returns the +train+ flag from the API response.
|
|
82
|
+
#
|
|
83
|
+
# +true+ means the API skipped real validation and forced +success+ to +true+ —
|
|
84
|
+
# typically because the sitekey does not exist, the secret does not match,
|
|
85
|
+
# or the sitekey's protection toggle is disabled. In production this means
|
|
86
|
+
# every submission appears successful regardless of whether the user solved
|
|
87
|
+
# the captcha. Check your sitekey and secret immediately if you see this.
|
|
88
|
+
#
|
|
89
|
+
# +false+ means normal operation and {#success_token?} reflects the real result.
|
|
90
|
+
#
|
|
91
|
+
# +nil+ means no API response was received (network failure) so the train
|
|
92
|
+
# state is unknown.
|
|
93
|
+
#
|
|
94
|
+
# @return [Boolean, nil]
|
|
95
|
+
def train
|
|
96
|
+
@state_train
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright 2026 Myra Security GmbH
|
|
4
|
+
#
|
|
5
|
+
# Redistribution and use in source and binary forms, with or without modification,
|
|
6
|
+
# are permitted provided that the following conditions are met:
|
|
7
|
+
#
|
|
8
|
+
# 1. Redistributions of source code must retain the above copyright notice,
|
|
9
|
+
# this list of conditions and the following disclaimer.
|
|
10
|
+
#
|
|
11
|
+
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
# this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
# and/or other materials provided with the distribution.
|
|
14
|
+
#
|
|
15
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
16
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
17
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
18
|
+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
19
|
+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
20
|
+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
21
|
+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
22
|
+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
23
|
+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
24
|
+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
25
|
+
# POSSIBILITY OF SUCH DAMAGE.
|
|
26
|
+
|
|
27
|
+
module EuCaptcha
|
|
28
|
+
VERSION = '1.0.0'
|
|
29
|
+
end
|
data/lib/eu_captcha.rb
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Copyright 2026 Myra Security GmbH
|
|
4
|
+
#
|
|
5
|
+
# Redistribution and use in source and binary forms, with or without modification,
|
|
6
|
+
# are permitted provided that the following conditions are met:
|
|
7
|
+
#
|
|
8
|
+
# 1. Redistributions of source code must retain the above copyright notice,
|
|
9
|
+
# this list of conditions and the following disclaimer.
|
|
10
|
+
#
|
|
11
|
+
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
# this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
# and/or other materials provided with the distribution.
|
|
14
|
+
#
|
|
15
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
16
|
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
17
|
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
18
|
+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
19
|
+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
20
|
+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
21
|
+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
22
|
+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
23
|
+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
24
|
+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
25
|
+
# POSSIBILITY OF SUCH DAMAGE.
|
|
26
|
+
|
|
27
|
+
require_relative 'eu_captcha/version'
|
|
28
|
+
require_relative 'eu_captcha/error'
|
|
29
|
+
require_relative 'eu_captcha/result'
|
|
30
|
+
require_relative 'eu_captcha/client'
|
metadata
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: eu_captcha
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Myra Security GmbH
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rspec
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '3.0'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '3.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: webmock
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '3.0'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '3.0'
|
|
40
|
+
description: EU captcha protects your website/API against abuse like form spam and
|
|
41
|
+
credential stuffing
|
|
42
|
+
executables: []
|
|
43
|
+
extensions: []
|
|
44
|
+
extra_rdoc_files: []
|
|
45
|
+
files:
|
|
46
|
+
- LICENSE
|
|
47
|
+
- README.md
|
|
48
|
+
- lib/eu_captcha.rb
|
|
49
|
+
- lib/eu_captcha/client.rb
|
|
50
|
+
- lib/eu_captcha/error.rb
|
|
51
|
+
- lib/eu_captcha/result.rb
|
|
52
|
+
- lib/eu_captcha/version.rb
|
|
53
|
+
homepage: https://eu-captcha.eu
|
|
54
|
+
licenses:
|
|
55
|
+
- BSD-2-Clause
|
|
56
|
+
metadata: {}
|
|
57
|
+
rdoc_options: []
|
|
58
|
+
require_paths:
|
|
59
|
+
- lib
|
|
60
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
61
|
+
requirements:
|
|
62
|
+
- - ">="
|
|
63
|
+
- !ruby/object:Gem::Version
|
|
64
|
+
version: '2.7'
|
|
65
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
66
|
+
requirements:
|
|
67
|
+
- - ">="
|
|
68
|
+
- !ruby/object:Gem::Version
|
|
69
|
+
version: '0'
|
|
70
|
+
requirements: []
|
|
71
|
+
rubygems_version: 3.6.7
|
|
72
|
+
specification_version: 4
|
|
73
|
+
summary: EU-Captcha server-side verification client for Ruby
|
|
74
|
+
test_files: []
|