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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eeded85d2f08978163e752d64342ca5e52fd1cf1fcaade7da0305caeb53e03b5
4
- data.tar.gz: 966b9eaf4f9d25549f0473471e446628af82c8ea160e5ebbefef518aa1a6fa08
3
+ metadata.gz: 5ff2c6d9ea446cd5631e2c3f4d3f27a0586a130acaf6bf9e10f9a7fb6613efc3
4
+ data.tar.gz: 38021e8c18baee9ff374c60d305cc6312cf024a2fff120e926cfa89d9d44ea52
5
5
  SHA512:
6
- metadata.gz: 0224226f06a4996e6034caabb6182f31d9a7848262a44bd8ed0571fe898a7706f4b95c46bdb0e5617c0bc04f948fc5dc8e5bffbb46cd20de04d9bdcacbe0b2fe
7
- data.tar.gz: 88e85eab06e2d351b6231f2769104b6d6a3320ae1cb2ccf68f432dad732a689ee5f50386ca69199534a76fb09425ddfd70f7d711b03b8e4d836e2b25415784ad
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/HTTP endpoints.
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 HTTP interaction.
287
+ Represents a captured DNS, HTTP or SMTP interaction.
278
288
 
279
289
  Attributes:
280
- - `type` (String) - Interaction type ("dns" or "http")
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
- attr_reader :server, :token
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
- def initialize(server:, token:)
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
- raise Error, 'Empty response body from server' if body.nil? || body.empty?
178
-
179
- JSON.parse(body)
183
+ parse_body(response)
180
184
  when 401
181
- raise AuthenticationError, "Authentication failed: #{body}"
185
+ raise AuthenticationError, "Authentication failed: #{error_excerpt(response)}"
182
186
  when 404
183
- raise NotFoundError, "Resource not found: #{body}"
187
+ raise NotFoundError, "Resource not found: #{error_excerpt(response)}"
184
188
  when 500..599
185
- raise ServerError, "Server error (#{response.status}): #{body}"
189
+ raise ServerError, "Server error (#{response.status}): #{error_excerpt(response)}"
186
190
  else
187
- raise Error, "Unexpected response (#{response.status}): #{body}"
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
@@ -15,4 +15,7 @@ module Hookd
15
15
 
16
16
  # Raised when the server returns a 5xx error
17
17
  class ServerError < Error; end
18
+
19
+ # Raised when a response exceeds the client's size limit
20
+ class ResponseTooLargeError < Error; end
18
21
  end
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 DNS and HTTP endpoints. expires_at and
5
- # metadata are populated for long-lived hooks (registered with a ttl) and nil
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']
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hookd
4
- # Represents a captured DNS or HTTP interaction
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Hookd
4
- VERSION = '1.3.0'
4
+ VERSION = '1.4.0'
5
5
  end
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.3.0
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 security
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: []