hookd-client 1.3.0 → 1.4.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 +16 -4
- data/lib/hookd/client.rb +44 -11
- data/lib/hookd/error.rb +3 -0
- data/lib/hookd/hook.rb +7 -5
- data/lib/hookd/interaction.rb +6 -1
- data/lib/hookd/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5ff2c6d9ea446cd5631e2c3f4d3f27a0586a130acaf6bf9e10f9a7fb6613efc3
|
|
4
|
+
data.tar.gz: 38021e8c18baee9ff374c60d305cc6312cf024a2fff120e926cfa89d9d44ea52
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3cc77be594c69256a2de2e31a23a7654dddbf94c50f0402fa327e3865e2fa25af4c4c4dfab6e9ca641aa611b20da6e22760646d570676a4a87e92d08382f3af8
|
|
7
|
+
data.tar.gz: 1d2da85d42bfff6ba7bcf2e1211b418c7bc8944ce8119169388e1b51cec085d64a54ceb4372ea5b5869714c792836cd193b1600b526c40021ead72cf8a492d28
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Hookd Ruby Client
|
|
2
2
|
|
|
3
|
-
Ruby client library for [Hookd](https://github.com/JoshuaMart/hookd/server), a DNS/HTTP interaction server for security testing and debugging.
|
|
3
|
+
Ruby client library for [Hookd](https://github.com/JoshuaMart/hookd/server), a DNS/HTTP/SMTP interaction server for security testing and debugging.
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
@@ -35,6 +35,7 @@ hook = client.register
|
|
|
35
35
|
puts "DNS endpoint: #{hook.dns}"
|
|
36
36
|
puts "HTTP endpoint: #{hook.http}"
|
|
37
37
|
puts "HTTPS endpoint: #{hook.https}"
|
|
38
|
+
puts "Mail endpoint: #{hook.smtp}" if hook.smtp
|
|
38
39
|
|
|
39
40
|
# Make a request to the HTTP endpoint to simulate an interaction
|
|
40
41
|
Typhoeus.get(hook.http)
|
|
@@ -46,6 +47,8 @@ interactions.each do |interaction|
|
|
|
46
47
|
puts "DNS query: #{interaction.data}"
|
|
47
48
|
elsif interaction.http?
|
|
48
49
|
puts "HTTP request: #{interaction.data}"
|
|
50
|
+
elsif interaction.smtp?
|
|
51
|
+
puts "Mail from #{interaction.data['mail_from']}: #{interaction.data['subject']}"
|
|
49
52
|
end
|
|
50
53
|
end
|
|
51
54
|
```
|
|
@@ -95,6 +98,8 @@ results.each do |hook_id, result|
|
|
|
95
98
|
puts " - DNS: #{interaction.data['qname']} (#{interaction.data['qtype']})"
|
|
96
99
|
elsif interaction.http?
|
|
97
100
|
puts " - HTTP: #{interaction.data['method']} #{interaction.data['path']}"
|
|
101
|
+
elsif interaction.smtp?
|
|
102
|
+
puts " - SMTP: #{interaction.data['mail_from']} (#{interaction.data['subject']})"
|
|
98
103
|
end
|
|
99
104
|
end
|
|
100
105
|
end
|
|
@@ -108,6 +113,10 @@ The client requires two configuration parameters:
|
|
|
108
113
|
- `server`: The Hookd server URL (e.g., `https://hookd.example.com`)
|
|
109
114
|
- `token`: Authentication token for API access
|
|
110
115
|
|
|
116
|
+
An optional `max_response_bytes:` caps the response payload the client will read
|
|
117
|
+
(64 MiB by default; zero or less disables it). Exceeding it raises
|
|
118
|
+
`Hookd::ResponseTooLargeError`.
|
|
119
|
+
|
|
111
120
|
### API Reference
|
|
112
121
|
|
|
113
122
|
#### `Hookd::Client`
|
|
@@ -116,7 +125,7 @@ Main client class for interacting with the Hookd server.
|
|
|
116
125
|
|
|
117
126
|
##### `#register(count: nil)`
|
|
118
127
|
|
|
119
|
-
Register one or more hooks and get DNS
|
|
128
|
+
Register one or more hooks and get their DNS, HTTP and (when the server runs a mail listener) SMTP endpoints.
|
|
120
129
|
|
|
121
130
|
**Single hook (default):**
|
|
122
131
|
```ruby
|
|
@@ -259,6 +268,7 @@ Attributes:
|
|
|
259
268
|
- `dns` (String) - DNS endpoint
|
|
260
269
|
- `http` (String) - HTTP endpoint
|
|
261
270
|
- `https` (String) - HTTPS endpoint
|
|
271
|
+
- `smtp` (String, nil) - Mail address (nil unless the server runs a mail listener)
|
|
262
272
|
- `created_at` (String) - Creation timestamp
|
|
263
273
|
- `expires_at` (String, nil) - Expiry timestamp (long-lived hooks)
|
|
264
274
|
- `metadata` (Hash, nil) - Metadata attached at registration
|
|
@@ -274,16 +284,17 @@ Attributes:
|
|
|
274
284
|
|
|
275
285
|
#### `Hookd::Interaction`
|
|
276
286
|
|
|
277
|
-
Represents a captured DNS or
|
|
287
|
+
Represents a captured DNS, HTTP or SMTP interaction.
|
|
278
288
|
|
|
279
289
|
Attributes:
|
|
280
|
-
- `type` (String) - Interaction type ("dns" or "
|
|
290
|
+
- `type` (String) - Interaction type ("dns", "http" or "smtp")
|
|
281
291
|
- `timestamp` (String) - When the interaction was captured
|
|
282
292
|
- `data` (Hash) - Interaction details
|
|
283
293
|
|
|
284
294
|
Methods:
|
|
285
295
|
- `#dns?` - Returns true if this is a DNS interaction
|
|
286
296
|
- `#http?` - Returns true if this is an HTTP interaction
|
|
297
|
+
- `#smtp?` - Returns true if this is an SMTP interaction
|
|
287
298
|
|
|
288
299
|
### Error Handling
|
|
289
300
|
|
|
@@ -307,3 +318,4 @@ Exception hierarchy:
|
|
|
307
318
|
- `Hookd::NotFoundError` - 404 Not Found
|
|
308
319
|
- `Hookd::ServerError` - 5xx Server Error
|
|
309
320
|
- `Hookd::ConnectionError` - Network/connection errors
|
|
321
|
+
- `Hookd::ResponseTooLargeError` - Response above `max_response_bytes`
|
data/lib/hookd/client.rb
CHANGED
|
@@ -6,11 +6,19 @@ require 'json'
|
|
|
6
6
|
module Hookd
|
|
7
7
|
# HTTP client for interacting with Hookd server
|
|
8
8
|
class Client
|
|
9
|
-
|
|
9
|
+
# Caps the payload materialised as a String. Generous, since a poll can
|
|
10
|
+
# return many interactions with full bodies. Zero or less disables it.
|
|
11
|
+
DEFAULT_MAX_RESPONSE_BYTES = 64 * 1024 * 1024
|
|
10
12
|
|
|
11
|
-
|
|
13
|
+
# How much of an error response is quoted back in the raised message.
|
|
14
|
+
ERROR_BODY_EXCERPT_BYTES = 1024
|
|
15
|
+
|
|
16
|
+
attr_reader :server, :token, :max_response_bytes
|
|
17
|
+
|
|
18
|
+
def initialize(server:, token:, max_response_bytes: DEFAULT_MAX_RESPONSE_BYTES)
|
|
12
19
|
@server = server
|
|
13
20
|
@token = token
|
|
21
|
+
@max_response_bytes = max_response_bytes
|
|
14
22
|
@http = HTTPX.with(
|
|
15
23
|
headers: { 'X-API-Key' => token },
|
|
16
24
|
timeout: {
|
|
@@ -170,24 +178,49 @@ module Hookd
|
|
|
170
178
|
raise ConnectionError, "Connection failed: #{error.message}"
|
|
171
179
|
end
|
|
172
180
|
|
|
173
|
-
body = response.body.to_s
|
|
174
|
-
|
|
175
181
|
case response.status
|
|
176
182
|
when 200, 201
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
JSON.parse(body)
|
|
183
|
+
parse_body(response)
|
|
180
184
|
when 401
|
|
181
|
-
raise AuthenticationError, "Authentication failed: #{
|
|
185
|
+
raise AuthenticationError, "Authentication failed: #{error_excerpt(response)}"
|
|
182
186
|
when 404
|
|
183
|
-
raise NotFoundError, "Resource not found: #{
|
|
187
|
+
raise NotFoundError, "Resource not found: #{error_excerpt(response)}"
|
|
184
188
|
when 500..599
|
|
185
|
-
raise ServerError, "Server error (#{response.status}): #{
|
|
189
|
+
raise ServerError, "Server error (#{response.status}): #{error_excerpt(response)}"
|
|
186
190
|
else
|
|
187
|
-
raise Error, "Unexpected response (#{response.status}): #{
|
|
191
|
+
raise Error, "Unexpected response (#{response.status}): #{error_excerpt(response)}"
|
|
188
192
|
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def parse_body(response)
|
|
196
|
+
body = read_body(response, @max_response_bytes)
|
|
197
|
+
raise Error, 'Empty response body from server' if body.empty?
|
|
198
|
+
|
|
199
|
+
JSON.parse(body)
|
|
189
200
|
rescue JSON::ParserError => e
|
|
190
201
|
raise Error, "Invalid JSON response: #{e.message}"
|
|
191
202
|
end
|
|
203
|
+
|
|
204
|
+
# Accumulates chunk by chunk and stops at the ceiling, raising unless
|
|
205
|
+
# truncate is set, in which case it returns the bounded slice.
|
|
206
|
+
def read_body(response, limit, truncate: false)
|
|
207
|
+
return response.body.to_s if limit <= 0
|
|
208
|
+
|
|
209
|
+
body = nil
|
|
210
|
+
response.body.each do |chunk|
|
|
211
|
+
# Seed from the first chunk to keep the payload's own encoding.
|
|
212
|
+
body = body ? body << chunk : chunk.dup
|
|
213
|
+
next if body.bytesize <= limit
|
|
214
|
+
return body.byteslice(0, limit).scrub if truncate
|
|
215
|
+
|
|
216
|
+
raise ResponseTooLargeError, "Response exceeds the #{limit} byte limit"
|
|
217
|
+
end
|
|
218
|
+
body || ''
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# A large error page must not become a large exception message.
|
|
222
|
+
def error_excerpt(response)
|
|
223
|
+
read_body(response, ERROR_BODY_EXCERPT_BYTES, truncate: true)
|
|
224
|
+
end
|
|
192
225
|
end
|
|
193
226
|
end
|
data/lib/hookd/error.rb
CHANGED
data/lib/hookd/hook.rb
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Hookd
|
|
4
|
-
# Represents a registered hook with
|
|
5
|
-
#
|
|
6
|
-
# otherwise.
|
|
4
|
+
# Represents a registered hook with its endpoints. smtp is set only when the
|
|
5
|
+
# server runs a mail listener. expires_at and metadata are populated for
|
|
6
|
+
# long-lived hooks (registered with a ttl) and nil otherwise.
|
|
7
7
|
class Hook
|
|
8
|
-
attr_reader :id, :dns, :http, :https, :created_at, :expires_at, :metadata
|
|
8
|
+
attr_reader :id, :dns, :http, :https, :smtp, :created_at, :expires_at, :metadata
|
|
9
9
|
|
|
10
|
-
def initialize(id:, dns:, http:, https:, created_at:, expires_at: nil, metadata: nil)
|
|
10
|
+
def initialize(id:, dns:, http:, https:, created_at:, smtp: nil, expires_at: nil, metadata: nil)
|
|
11
11
|
@id = id
|
|
12
12
|
@dns = dns
|
|
13
13
|
@http = http
|
|
14
14
|
@https = https
|
|
15
|
+
@smtp = smtp
|
|
15
16
|
@created_at = created_at
|
|
16
17
|
@expires_at = expires_at
|
|
17
18
|
@metadata = metadata
|
|
@@ -26,6 +27,7 @@ module Hookd
|
|
|
26
27
|
dns: hash['dns'],
|
|
27
28
|
http: hash['http'],
|
|
28
29
|
https: hash['https'],
|
|
30
|
+
smtp: hash['smtp'],
|
|
29
31
|
created_at: hash['created_at'],
|
|
30
32
|
expires_at: hash['expires_at'],
|
|
31
33
|
metadata: hash['metadata']
|
data/lib/hookd/interaction.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Hookd
|
|
4
|
-
# Represents a captured DNS or
|
|
4
|
+
# Represents a captured DNS, HTTP or SMTP interaction
|
|
5
5
|
class Interaction
|
|
6
6
|
attr_reader :type, :timestamp, :source_ip, :data
|
|
7
7
|
|
|
@@ -32,6 +32,11 @@ module Hookd
|
|
|
32
32
|
type == 'http'
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
# Check if this is an SMTP interaction
|
|
36
|
+
def smtp?
|
|
37
|
+
type == 'smtp'
|
|
38
|
+
end
|
|
39
|
+
|
|
35
40
|
def to_s
|
|
36
41
|
"#<Hookd::Interaction type=#{type} timestamp=#{timestamp} source_ip=#{source_ip}>"
|
|
37
42
|
end
|
data/lib/hookd/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hookd-client
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Joshua MARTINELLE
|
|
@@ -23,8 +23,8 @@ dependencies:
|
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '1.0'
|
|
26
|
-
description: Ruby client library for Hookd, a DNS/HTTP interaction server for
|
|
27
|
-
testing and debugging
|
|
26
|
+
description: Ruby client library for Hookd, a DNS/SMTP/HTTP interaction server for
|
|
27
|
+
security testing and debugging
|
|
28
28
|
email:
|
|
29
29
|
- contact@jomar.fr
|
|
30
30
|
executables: []
|