hookd-client 1.0.0 → 1.2.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 +4 -4
- data/README.md +105 -2
- data/lib/hookd/client.rb +72 -29
- data/lib/hookd/version.rb +1 -1
- metadata +16 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 97edc24416cdc28af2b3e6506d598711705cfc948bbb24416256343ed07e70d9
|
|
4
|
+
data.tar.gz: '008e6b1dd48ea295b47551c1cf9182ea4beceacff62751db9ee0829157911378'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: baefacc1c8e98030276e92c3ea4c7ab3e78bf8bef6465e0a99c4574ec221aae4070d3e5c10e353fb4b732532aa19031cc5697dab4e355fe984f730fcc46ecc97
|
|
7
|
+
data.tar.gz: c382ec4a08173ecead4a6334b095a112c3fce9eed944216b48634652dfc20b33d0a820d380920e6682c987f8ad718a0e906d015c94603ca87898da4c93ab83bd
|
data/README.md
CHANGED
|
@@ -39,7 +39,7 @@ puts "HTTPS endpoint: #{hook.https}"
|
|
|
39
39
|
# Make a request to the HTTP endpoint to simulate an interaction
|
|
40
40
|
Typhoeus.get(hook.http)
|
|
41
41
|
|
|
42
|
-
# Poll for interactions
|
|
42
|
+
# Poll for interactions (single hook)
|
|
43
43
|
interactions = client.poll(hook.id)
|
|
44
44
|
interactions.each do |interaction|
|
|
45
45
|
if interaction.dns?
|
|
@@ -50,6 +50,60 @@ interactions.each do |interaction|
|
|
|
50
50
|
end
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
+
### Batch Polling Example
|
|
54
|
+
|
|
55
|
+
When working with multiple hooks, use `poll_batch` for better performance:
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
require 'hookd'
|
|
59
|
+
require 'typhoeus'
|
|
60
|
+
|
|
61
|
+
# Initialize the client
|
|
62
|
+
client = Hookd::Client.new(
|
|
63
|
+
server: "https://hookd.example.com",
|
|
64
|
+
token: ENV['HOOKD_TOKEN']
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Register multiple hooks
|
|
68
|
+
puts "Registering 5 hooks..."
|
|
69
|
+
hooks = client.register(count: 5)
|
|
70
|
+
|
|
71
|
+
hook_ids = hooks.map(&:id)
|
|
72
|
+
puts "Created hooks: #{hook_ids.join(', ')}"
|
|
73
|
+
|
|
74
|
+
# Simulate some interactions...
|
|
75
|
+
# (make DNS queries, HTTP requests, etc.)
|
|
76
|
+
|
|
77
|
+
# Simulate some interactions
|
|
78
|
+
puts "\nSimulating HTTP requests..."
|
|
79
|
+
hooks.each do |hook|
|
|
80
|
+
Typhoeus.get(hook.http)
|
|
81
|
+
puts " ✓ GET #{hook.http}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Batch poll all hooks at once (1 HTTP request instead of 5)
|
|
85
|
+
puts "Batch polling #{hook_ids.size} hooks..."
|
|
86
|
+
results = client.poll_batch(hook_ids)
|
|
87
|
+
|
|
88
|
+
# Display results
|
|
89
|
+
results.each do |hook_id, result|
|
|
90
|
+
if result[:error]
|
|
91
|
+
puts "❌ Hook #{hook_id}: #{result[:error]}"
|
|
92
|
+
else
|
|
93
|
+
interactions = result[:interactions]
|
|
94
|
+
puts "✅ Hook #{hook_id}: #{interactions.size} interaction(s)"
|
|
95
|
+
|
|
96
|
+
interactions.each do |interaction|
|
|
97
|
+
if interaction.dns?
|
|
98
|
+
puts " - DNS: #{interaction.data['qname']} (#{interaction.data['qtype']})"
|
|
99
|
+
elsif interaction.http?
|
|
100
|
+
puts " - HTTP: #{interaction.data['method']} #{interaction.data['path']}"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
```
|
|
106
|
+
|
|
53
107
|
### Configuration
|
|
54
108
|
|
|
55
109
|
The client requires two configuration parameters:
|
|
@@ -94,7 +148,7 @@ Raises:
|
|
|
94
148
|
|
|
95
149
|
##### `#poll(hook_id)`
|
|
96
150
|
|
|
97
|
-
Poll for interactions captured by a hook.
|
|
151
|
+
Poll for interactions captured by a single hook.
|
|
98
152
|
|
|
99
153
|
```ruby
|
|
100
154
|
interactions = client.poll("abc123")
|
|
@@ -112,6 +166,55 @@ Raises:
|
|
|
112
166
|
- `Hookd::ServerError` - Server error (5xx)
|
|
113
167
|
- `Hookd::ConnectionError` - Connection failed
|
|
114
168
|
|
|
169
|
+
##### `#poll_batch(hook_ids)`
|
|
170
|
+
|
|
171
|
+
**Batch poll** - Poll for interactions from multiple hooks in a single request.
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
# Register multiple hooks
|
|
175
|
+
hooks = client.register(count: 3)
|
|
176
|
+
hook_ids = hooks.map(&:id)
|
|
177
|
+
|
|
178
|
+
# Batch poll all hooks at once (1 HTTP request instead of 3)
|
|
179
|
+
results = client.poll_batch(hook_ids)
|
|
180
|
+
# => {
|
|
181
|
+
# "abc123" => { interactions: [...], error: nil },
|
|
182
|
+
# "def456" => { interactions: [...], error: nil },
|
|
183
|
+
# "ghi789" => { interactions: [], error: nil }
|
|
184
|
+
# }
|
|
185
|
+
|
|
186
|
+
# Process results
|
|
187
|
+
results.each do |hook_id, result|
|
|
188
|
+
if result[:error]
|
|
189
|
+
puts "Error for #{hook_id}: #{result[:error]}"
|
|
190
|
+
else
|
|
191
|
+
puts "Hook #{hook_id}: #{result[:interactions].size} interactions"
|
|
192
|
+
result[:interactions].each do |interaction|
|
|
193
|
+
puts " - #{interaction.type}: #{interaction.data}"
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Parameters:
|
|
200
|
+
- `hook_ids` (Array<String>) - Array of hook IDs to poll
|
|
201
|
+
|
|
202
|
+
Returns: Hash mapping hook IDs to results
|
|
203
|
+
- Each result contains:
|
|
204
|
+
- `interactions` (Array<Hookd::Interaction>) - Array of interactions
|
|
205
|
+
- `error` (String, nil) - Error message if hook not found
|
|
206
|
+
|
|
207
|
+
Raises:
|
|
208
|
+
- `ArgumentError` - Invalid hook_ids (not an array or empty)
|
|
209
|
+
- `Hookd::AuthenticationError` - Authentication failed
|
|
210
|
+
- `Hookd::ServerError` - Server error (5xx)
|
|
211
|
+
- `Hookd::ConnectionError` - Connection failed
|
|
212
|
+
|
|
213
|
+
**Benefits:**
|
|
214
|
+
- **Performance**: Reduced latency with single HTTP request
|
|
215
|
+
- **Efficiency**: Automatic connection reuse with HTTPX
|
|
216
|
+
- **Atomic**: Consistent snapshot of all hooks
|
|
217
|
+
|
|
115
218
|
##### `#metrics`
|
|
116
219
|
|
|
117
220
|
Get server metrics (requires authentication).
|
data/lib/hookd/client.rb
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require '
|
|
3
|
+
require 'httpx'
|
|
4
4
|
require 'json'
|
|
5
|
-
require 'uri'
|
|
6
5
|
|
|
7
6
|
module Hookd
|
|
8
7
|
# HTTP client for interacting with Hookd server
|
|
@@ -12,7 +11,13 @@ module Hookd
|
|
|
12
11
|
def initialize(server:, token:)
|
|
13
12
|
@server = server
|
|
14
13
|
@token = token
|
|
15
|
-
@
|
|
14
|
+
@http = HTTPX.with(
|
|
15
|
+
headers: { 'X-API-Key' => token },
|
|
16
|
+
timeout: {
|
|
17
|
+
connect_timeout: 10,
|
|
18
|
+
read_timeout: 30
|
|
19
|
+
}
|
|
20
|
+
)
|
|
16
21
|
end
|
|
17
22
|
|
|
18
23
|
# Register one or more hooks
|
|
@@ -25,9 +30,7 @@ module Hookd
|
|
|
25
30
|
def register(count: nil)
|
|
26
31
|
body = count.nil? ? nil : { count: count }
|
|
27
32
|
|
|
28
|
-
if count && (!count.is_a?(Integer) || count < 1)
|
|
29
|
-
raise ArgumentError, 'count must be a positive integer'
|
|
30
|
-
end
|
|
33
|
+
raise ArgumentError, 'count must be a positive integer' if count && (!count.is_a?(Integer) || count < 1)
|
|
31
34
|
|
|
32
35
|
response = post('/register', body)
|
|
33
36
|
|
|
@@ -59,6 +62,27 @@ module Hookd
|
|
|
59
62
|
raise Error, "Invalid response format: #{e.message}"
|
|
60
63
|
end
|
|
61
64
|
|
|
65
|
+
# Poll for interactions on multiple hooks (batch)
|
|
66
|
+
# @param hook_ids [Array<String>] the hook IDs to poll
|
|
67
|
+
# @return [Hash<String, Hash>] hash mapping hook_id to results
|
|
68
|
+
# Results format: { "hook_id" => { interactions: [...], error: "..." } }
|
|
69
|
+
# @raise [Hookd::AuthenticationError] if authentication fails
|
|
70
|
+
# @raise [Hookd::ServerError] if server returns 5xx
|
|
71
|
+
# @raise [Hookd::ConnectionError] if connection fails
|
|
72
|
+
# @raise [ArgumentError] if hook_ids is invalid
|
|
73
|
+
def poll_batch(hook_ids)
|
|
74
|
+
validate_hook_ids(hook_ids)
|
|
75
|
+
|
|
76
|
+
url = "#{@server}/poll"
|
|
77
|
+
options = { headers: { 'Content-Type' => 'application/json' }, json: hook_ids }
|
|
78
|
+
response = @http.post(url, **options)
|
|
79
|
+
response_data = handle_response(response)
|
|
80
|
+
|
|
81
|
+
transform_batch_results(response_data['results'])
|
|
82
|
+
rescue NoMethodError => e
|
|
83
|
+
raise Error, "Invalid response format: #{e.message}"
|
|
84
|
+
end
|
|
85
|
+
|
|
62
86
|
# Get server metrics (requires authentication)
|
|
63
87
|
# @return [Hash] metrics data
|
|
64
88
|
# @raise [Hookd::AuthenticationError] if authentication fails
|
|
@@ -71,43 +95,62 @@ module Hookd
|
|
|
71
95
|
private
|
|
72
96
|
|
|
73
97
|
def get(path)
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
98
|
+
url = "#{@server}#{path}"
|
|
99
|
+
response = @http.get(url)
|
|
100
|
+
handle_response(response)
|
|
77
101
|
end
|
|
78
102
|
|
|
79
103
|
def post(path, body = nil)
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
104
|
+
url = "#{@server}#{path}"
|
|
105
|
+
options = { headers: { 'Content-Type' => 'application/json' } }
|
|
106
|
+
options[:json] = body if body
|
|
107
|
+
|
|
108
|
+
response = @http.post(url, **options)
|
|
109
|
+
handle_response(response)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def validate_hook_ids(hook_ids)
|
|
113
|
+
raise ArgumentError, 'hook_ids must be an array' unless hook_ids.is_a?(Array)
|
|
114
|
+
raise ArgumentError, 'hook_ids cannot be empty' if hook_ids.empty?
|
|
85
115
|
end
|
|
86
116
|
|
|
87
|
-
def
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
117
|
+
def transform_batch_results(results)
|
|
118
|
+
return {} if results.nil? || !results.is_a?(Hash)
|
|
119
|
+
|
|
120
|
+
results.transform_values do |result|
|
|
121
|
+
next result if result['error']
|
|
122
|
+
|
|
123
|
+
interactions = result['interactions']
|
|
124
|
+
{
|
|
125
|
+
interactions: interactions&.map { |i| Interaction.from_hash(i) } || [],
|
|
126
|
+
error: result['error']
|
|
127
|
+
}
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def handle_response(response)
|
|
132
|
+
# HTTPX returns HTTPX::ErrorResponse for connection/timeout errors
|
|
133
|
+
if response.is_a?(HTTPX::ErrorResponse)
|
|
134
|
+
error = response.error
|
|
135
|
+
raise ConnectionError, "Connection failed: #{error.message}"
|
|
136
|
+
end
|
|
92
137
|
|
|
93
|
-
|
|
138
|
+
body = response.body.to_s
|
|
94
139
|
|
|
95
|
-
case response.
|
|
140
|
+
case response.status
|
|
96
141
|
when 200, 201
|
|
97
|
-
raise Error, 'Empty response body from server' if
|
|
142
|
+
raise Error, 'Empty response body from server' if body.nil? || body.empty?
|
|
98
143
|
|
|
99
|
-
JSON.parse(
|
|
144
|
+
JSON.parse(body)
|
|
100
145
|
when 401
|
|
101
|
-
raise AuthenticationError, "Authentication failed: #{
|
|
146
|
+
raise AuthenticationError, "Authentication failed: #{body}"
|
|
102
147
|
when 404
|
|
103
|
-
raise NotFoundError, "Resource not found: #{
|
|
148
|
+
raise NotFoundError, "Resource not found: #{body}"
|
|
104
149
|
when 500..599
|
|
105
|
-
raise ServerError, "Server error (#{response.
|
|
150
|
+
raise ServerError, "Server error (#{response.status}): #{body}"
|
|
106
151
|
else
|
|
107
|
-
raise Error, "Unexpected response (#{response.
|
|
152
|
+
raise Error, "Unexpected response (#{response.status}): #{body}"
|
|
108
153
|
end
|
|
109
|
-
rescue SocketError, Errno::ECONNREFUSED, Net::OpenTimeout, Net::ReadTimeout => e
|
|
110
|
-
raise ConnectionError, "Connection failed: #{e.message}"
|
|
111
154
|
rescue JSON::ParserError => e
|
|
112
155
|
raise Error, "Invalid JSON response: #{e.message}"
|
|
113
156
|
end
|
data/lib/hookd/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hookd-client
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Joshua MARTINELLE
|
|
8
8
|
bindir: bin
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
-
dependencies:
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: httpx
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '1.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '1.0'
|
|
12
26
|
description: Ruby client library for Hookd, a DNS/HTTP interaction server for security
|
|
13
27
|
testing and debugging
|
|
14
28
|
email:
|