fair_champion_harvester 0.1.14 → 0.1.15
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/CHANGELOG.md +7 -0
- data/lib/extruct.rb +25 -1
- data/lib/fair_champion_harvester/version.rb +1 -1
- data/lib/fair_champion_harvester.rb +1 -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: d47d4ea410de9aeb76e8e746409aaa0fa07e9fd6941cbd6bb5075d0639d34622
|
|
4
|
+
data.tar.gz: d9eb03395dc25d6025dcbf849b56de37561d839b267bdaaf6aa365fbd42bc51c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5a4a6dd6d40cd24ea5e4b0e555f52d09d40cb8d45b5fe4e50456f29b537f9274b8fd22a799cf44103e718c185722786529336edccbb19397b8ea3d40e054e870
|
|
7
|
+
data.tar.gz: 51f912518ea9e126de18d8e2199faa451e24afb92cc4b57461bc0ccb89e747c35e1bf8944a76e668a6b2f74eb12481850d1f5c653db4e934749e918f4d4b380d
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.1.15] - 2026-08-05
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- `Extruct.do_extruct` had no timeout on the external `extruct` subprocess (`Open3.capture3`). The content-type and magic-byte checks catch most binary responses, but not all (unreliable/missing headers, formats outside `BINARY_MAGIC_BYTES`, or just very large/slow-to-parse HTML) — a single such request blocked the calling thread indefinitely and leaked an orphaned Python process, since Puma's `worker_timeout` only restarts a worker whose *every* thread has stopped responding, not one with a single permanently-stuck thread among several. Diagnosed on `tests.ostrails.eu` after multiple hours-long outages: load average 40-50, several `python3 extruct <uri>` processes pegged at high CPU for many minutes, and 68 zombie processes. Fixed by prepending the `timeout` coreutil (`-k 5`: escalate to `SIGKILL` 5s after the initial `SIGTERM` if ignored) to the invocation, configurable via `EXTRUCT_TIMEOUT_SECONDS` (default 30s).
|
|
10
|
+
- Same change also closes a latent command-injection hole: the subprocess was previously invoked as a single shell-interpolated string (`"#{ExtructCommand} #{uri}"`), so a crafted `uri` could inject shell metacharacters. It's now passed as separate `argv` elements (`Open3.capture3("timeout", ..., *command_parts, uri)`), which never goes through a shell.
|
|
11
|
+
|
|
5
12
|
## [0.1.14] - 2026-06-30
|
|
6
13
|
|
|
7
14
|
### Fixed
|
data/lib/extruct.rb
CHANGED
|
@@ -8,6 +8,15 @@ module FAIRChampionHarvester
|
|
|
8
8
|
}ix
|
|
9
9
|
BINARY_MAGIC_BYTES = /\A(%PDF|PK\x03\x04|GIF8|\x89PNG|\xFF\xD8\xFF|\xD0\xCF|\x1F\x8B)/n
|
|
10
10
|
|
|
11
|
+
# Hard ceiling, in seconds, on how long the external `extruct` process may
|
|
12
|
+
# run. The content-type and magic-byte checks above catch most binary
|
|
13
|
+
# responses, but not all of them (unreliable/missing headers, formats
|
|
14
|
+
# outside BINARY_MAGIC_BYTES, or just very large/slow-to-parse HTML) —
|
|
15
|
+
# without this, a single such request blocks the calling thread (and
|
|
16
|
+
# leaks an orphaned Python process) indefinitely. Configurable via
|
|
17
|
+
# EXTRUCT_TIMEOUT_SECONDS.
|
|
18
|
+
TIMEOUT_SECONDS = ENV.fetch("EXTRUCT_TIMEOUT_SECONDS", 30).to_i
|
|
19
|
+
|
|
11
20
|
def self.do_extruct(meta, uri, content_type: nil, body_prefix: nil)
|
|
12
21
|
if content_type&.match?(BINARY_CONTENT_TYPE)
|
|
13
22
|
meta.comments << "INFO: Skipping extruct for #{uri} — " \
|
|
@@ -21,10 +30,25 @@ module FAIRChampionHarvester
|
|
|
21
30
|
meta.comments << "INFO: Using 'extruct' to try to extract metadata from return value (message body) of #{uri}.\n"
|
|
22
31
|
warn "begin open3"
|
|
23
32
|
# binding.pry
|
|
24
|
-
|
|
33
|
+
# Pass argv as separate elements (not one interpolated string) so this
|
|
34
|
+
# never goes through a shell — `uri` can be attacker/publisher-supplied
|
|
35
|
+
# and must not be shell-interpolated — and prepend the `timeout`
|
|
36
|
+
# coreutil so a hung/slow extruct process is forcibly killed (-k 5:
|
|
37
|
+
# escalate to SIGKILL 5s after the initial SIGTERM if it's ignored)
|
|
38
|
+
# instead of blocking this thread and leaking a child process forever.
|
|
39
|
+
command_parts = Shellwords.split(FAIRChampionHarvester::Utils::ExtructCommand)
|
|
40
|
+
stdout, stderr, status = Open3.capture3(
|
|
41
|
+
"timeout", "-k", "5", TIMEOUT_SECONDS.to_s, *command_parts, uri
|
|
42
|
+
)
|
|
25
43
|
warn ""
|
|
26
44
|
# sleep 5
|
|
27
45
|
warn "open3 status: #{status} #{stdout}"
|
|
46
|
+
|
|
47
|
+
if [124, 137].include?(status.exitstatus)
|
|
48
|
+
meta.comments << "WARN: extruct timed out after #{TIMEOUT_SECONDS}s parsing #{uri} — skipping.\n"
|
|
49
|
+
return
|
|
50
|
+
end
|
|
51
|
+
|
|
28
52
|
result = stderr # absurd that the output comes over stderr! LOL!
|
|
29
53
|
|
|
30
54
|
# result = %x{#{FAIRChampionHarvester::Utils::ExtructCommand} #{uri} 2>&1}
|