tcat 0.3.5 → 0.4.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 +4 -4
- data/README.md +21 -3
- data/lib/tcat/encryption_service.rb +3 -0
- data/lib/tcat/query.rb +31 -11
- data/lib/tcat/version.rb +1 -1
- data/lib/tcat/worker_client.rb +14 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 588b8dd9b49b05b35d28384cb85c3bc4e87b38852a04c3da8ce5b35ce59739bc
|
|
4
|
+
data.tar.gz: cd704ade306674058f6fd4c8de240d7570f54dba29ad7e1dfe30afb3b0119d40
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8617ad7c2b5bec16c3d7c94fd39175b45417a869feb6a74f0a1fab3590860c44d4a7e61a49b908982ae65f4b856504de3e61c9f96ba082c0ef0736cf936a59ca
|
|
7
|
+
data.tar.gz: '059ea7b7711cae4a2d146a23470016048edfb6e5c5d8951822ba17be8bd792355b1f9d29a73cc1b40e604884b332ac1548e453fb66f3cc22cff6c43198c510c7'
|
data/README.md
CHANGED
|
@@ -64,11 +64,16 @@ Tcat.configure do |config|
|
|
|
64
64
|
config.secret_key = 'your_secret_key'
|
|
65
65
|
end
|
|
66
66
|
|
|
67
|
-
#
|
|
68
|
-
query = Tcat::Query.new('your_tracking_number')
|
|
67
|
+
# Two equivalent shapes — pick whichever fits your code:
|
|
69
68
|
|
|
70
|
-
#
|
|
69
|
+
# A) tracking number bound at construction (one-shot)
|
|
70
|
+
query = Tcat::Query.new('your_tracking_number')
|
|
71
71
|
status = query.status_code
|
|
72
|
+
|
|
73
|
+
# B) tracking number per call (reuse one client for many lookups,
|
|
74
|
+
# matches Tcat::WorkerClient's shape)
|
|
75
|
+
query = Tcat::Query.new
|
|
76
|
+
status = query.status_code('your_tracking_number')
|
|
72
77
|
# Returns one of the following:
|
|
73
78
|
# :done - Successfully delivered
|
|
74
79
|
# :delivering - Out for delivery
|
|
@@ -265,6 +270,19 @@ This gem is available as open source under the terms of the [MIT License](https:
|
|
|
265
270
|
|
|
266
271
|
## Changelog
|
|
267
272
|
|
|
273
|
+
### 0.4.1
|
|
274
|
+
|
|
275
|
+
- Security hardening: `Tcat::Query` now uses `SecureRandom` (CSPRNG) instead of `rand` for the per-request freshness component embedded in the encrypted `secret` payload
|
|
276
|
+
- Encryption: `Tcat::EncryptionService` explicitly disables OpenSSL's built-in cipher padding so the manually applied PKCS#7 padding cannot be double-applied if the cipher pipeline ever changes
|
|
277
|
+
- `Tcat::WorkerClient` warns when `worker_url` uses `http://` against a non-localhost host so a misconfigured deployment cannot silently leak the Bearer token over plaintext
|
|
278
|
+
- Worker: added `[observability.logs]` (`enabled = false`, `invocation_logs = true`) to `wrangler.toml`
|
|
279
|
+
|
|
280
|
+
### 0.4.0
|
|
281
|
+
|
|
282
|
+
- `Tcat::Query#status_code`, `#history`, `#latest_status` now accept an optional tracking-number argument, mirroring `Tcat::WorkerClient`'s shape
|
|
283
|
+
- `Tcat::Query.new` may be called without a tracking number for stateless reuse: `Tcat::Query.new.status_code('1234567890')`
|
|
284
|
+
- Existing one-shot construction (`Tcat::Query.new(tn).status_code`) is still supported
|
|
285
|
+
|
|
268
286
|
### 0.3.5
|
|
269
287
|
|
|
270
288
|
- Added optional `worker_url` and `worker_token` to `Tcat.configure`; explicit `Tcat::WorkerClient.new(url, token:)` arguments still take precedence
|
|
@@ -34,6 +34,9 @@ module Tcat
|
|
|
34
34
|
def setup_cipher
|
|
35
35
|
cipher = OpenSSL::Cipher.new(CIPHER_ALGORITHM)
|
|
36
36
|
cipher.encrypt
|
|
37
|
+
# PKCS#7 padding is applied manually in #pad_message; turn off OpenSSL's
|
|
38
|
+
# built-in padding so a future cipher.final call cannot double-pad.
|
|
39
|
+
cipher.padding = 0
|
|
37
40
|
cipher.key = @secret_key
|
|
38
41
|
cipher
|
|
39
42
|
end
|
data/lib/tcat/query.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'net/http'
|
|
4
4
|
require 'base64'
|
|
5
|
+
require 'securerandom'
|
|
5
6
|
require 'ox'
|
|
6
7
|
require_relative 'http_client'
|
|
7
8
|
require_relative 'encryption_service'
|
|
@@ -11,7 +12,11 @@ module Tcat
|
|
|
11
12
|
class Query
|
|
12
13
|
DeliveryItem = Struct.new(:status, :status_code, :time, :office, :last_update, keyword_init: true)
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
# @param tracking_number [String, nil] Default tracking number used when
|
|
16
|
+
# the per-call methods are invoked without an explicit argument. Pass nil
|
|
17
|
+
# to construct a stateless client and supply the tracking number on each
|
|
18
|
+
# call (matching Tcat::WorkerClient's shape).
|
|
19
|
+
def initialize(tracking_number = nil)
|
|
15
20
|
@secret_string = Tcat.configuration.secret_string
|
|
16
21
|
@secret_key = Tcat.configuration.secret_key
|
|
17
22
|
validate_secrets!
|
|
@@ -25,22 +30,27 @@ module Tcat
|
|
|
25
30
|
end
|
|
26
31
|
|
|
27
32
|
# Get current delivery status code
|
|
33
|
+
# @param tracking_number [String, nil] Overrides the constructor value.
|
|
28
34
|
# @return [Symbol] Status code (:done, :delivering, :collected, :in_transit, :unknown)
|
|
29
|
-
def status_code
|
|
30
|
-
|
|
35
|
+
def status_code(tracking_number = nil)
|
|
36
|
+
tn = resolve_tracking_number(tracking_number)
|
|
37
|
+
response_body = @http_client.post(data(tn))
|
|
31
38
|
parse_status_code(response_body) if response_body
|
|
32
|
-
rescue HttpClient::RequestError
|
|
39
|
+
rescue HttpClient::RequestError
|
|
33
40
|
# Log error or handle it appropriately
|
|
34
41
|
nil
|
|
35
42
|
end
|
|
36
43
|
|
|
37
44
|
# Get complete delivery history
|
|
45
|
+
# @param tracking_number [String, nil] Overrides the constructor value.
|
|
38
46
|
# @return [Array<DeliveryItem>] Array of delivery status items, sorted by time (newest first)
|
|
39
|
-
def history
|
|
40
|
-
|
|
47
|
+
def history(tracking_number = nil)
|
|
48
|
+
tn = resolve_tracking_number(tracking_number)
|
|
49
|
+
response_body = @http_client.post(data(tn))
|
|
41
50
|
if response_body
|
|
42
51
|
result = Ox.load(response_body, mode: :hash, with_cdata: true)
|
|
43
52
|
return [] if result.dig(:Result, :Status) != '0'
|
|
53
|
+
|
|
44
54
|
extract_delivery_history(result)
|
|
45
55
|
end
|
|
46
56
|
rescue StandardError => e
|
|
@@ -49,10 +59,12 @@ module Tcat
|
|
|
49
59
|
end
|
|
50
60
|
|
|
51
61
|
# Get latest delivery status with details
|
|
62
|
+
# @param tracking_number [String, nil] Overrides the constructor value.
|
|
52
63
|
# @return [DeliveryItem, nil] Latest delivery status or nil if no history
|
|
53
|
-
def latest_status
|
|
54
|
-
items = history
|
|
64
|
+
def latest_status(tracking_number = nil)
|
|
65
|
+
items = history(tracking_number)
|
|
55
66
|
return nil if items.empty?
|
|
67
|
+
|
|
56
68
|
items.first
|
|
57
69
|
end
|
|
58
70
|
|
|
@@ -75,9 +87,16 @@ module Tcat
|
|
|
75
87
|
warn e.backtrace.first(5).join("\n") if $DEBUG
|
|
76
88
|
end
|
|
77
89
|
|
|
78
|
-
def
|
|
90
|
+
def resolve_tracking_number(arg)
|
|
91
|
+
tn = arg || @tracking_number
|
|
92
|
+
raise ArgumentError, 'tracking number required' if tn.nil? || tn.to_s.empty?
|
|
93
|
+
|
|
94
|
+
tn
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def data(tracking_number)
|
|
79
98
|
{
|
|
80
|
-
ConsignmentNo:
|
|
99
|
+
ConsignmentNo: tracking_number,
|
|
81
100
|
f: 5,
|
|
82
101
|
isForeign: 'N',
|
|
83
102
|
secret: generate_secret,
|
|
@@ -90,7 +109,8 @@ module Tcat
|
|
|
90
109
|
end
|
|
91
110
|
|
|
92
111
|
def random
|
|
93
|
-
|
|
112
|
+
# CSPRNG so the per-request freshness component is unpredictable.
|
|
113
|
+
(SecureRandom.random_number(90_000) + 10_000).to_s
|
|
94
114
|
end
|
|
95
115
|
|
|
96
116
|
def source_string
|
data/lib/tcat/version.rb
CHANGED
data/lib/tcat/worker_client.rb
CHANGED
|
@@ -99,15 +99,29 @@ module Tcat
|
|
|
99
99
|
|
|
100
100
|
private
|
|
101
101
|
|
|
102
|
+
LOCAL_HOSTS = %w[localhost 127.0.0.1 ::1].freeze
|
|
103
|
+
private_constant :LOCAL_HOSTS
|
|
104
|
+
|
|
102
105
|
def validate_url!
|
|
103
106
|
uri = URI.parse(@worker_url)
|
|
104
107
|
unless %w[http https].include?(uri.scheme)
|
|
105
108
|
raise ArgumentError, 'Invalid Worker URL: must be http or https'
|
|
106
109
|
end
|
|
110
|
+
|
|
111
|
+
warn_if_insecure(uri)
|
|
107
112
|
rescue URI::InvalidURIError => e
|
|
108
113
|
raise ArgumentError, "Invalid Worker URL: #{e.message}"
|
|
109
114
|
end
|
|
110
115
|
|
|
116
|
+
def warn_if_insecure(uri)
|
|
117
|
+
return if uri.scheme == 'https'
|
|
118
|
+
return if LOCAL_HOSTS.include?(uri.host)
|
|
119
|
+
|
|
120
|
+
warn '[Tcat::WorkerClient] WARNING: worker_url uses http://; ' \
|
|
121
|
+
'requests and any Authorization token will be sent in plaintext. ' \
|
|
122
|
+
'Use https:// in production.'
|
|
123
|
+
end
|
|
124
|
+
|
|
111
125
|
def make_request(uri)
|
|
112
126
|
response = setup_http(uri).request(build_request(uri))
|
|
113
127
|
raise APIError, "HTTP #{response.code}: #{response.message}" unless response.is_a?(Net::HTTPSuccess)
|