bidi2pdf 0.1.1 → 0.1.2
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/.rubocop.yml +3 -0
- data/CHANGELOG.md +29 -3
- data/README.md +10 -0
- data/Rakefile +2 -0
- data/cliff.toml +114 -0
- data/docker/Dockerfile.chromedriver +33 -0
- data/docker/docker-compose.yml +7 -0
- data/docker/entrypoint.sh +3 -0
- data/lib/bidi2pdf/bidi/add_headers_interceptor.rb +1 -1
- data/lib/bidi2pdf/bidi/auth_interceptor.rb +1 -1
- data/lib/bidi2pdf/bidi/client.rb +55 -140
- data/lib/bidi2pdf/bidi/command_manager.rb +82 -0
- data/lib/bidi2pdf/bidi/connection_manager.rb +34 -0
- data/lib/bidi2pdf/bidi/session.rb +26 -9
- data/lib/bidi2pdf/chromedriver_manager.rb +19 -4
- data/lib/bidi2pdf/cli.rb +147 -18
- data/lib/bidi2pdf/launcher.rb +19 -6
- data/lib/bidi2pdf/version.rb +1 -1
- data/sig/bidi2pdf/bidi/add_headers_interceptor.rbs +20 -0
- data/sig/bidi2pdf/bidi/auth_interceptor.rbs +17 -0
- data/sig/bidi2pdf/bidi/browser.rbs +38 -0
- data/sig/bidi2pdf/bidi/browser_tab.rbs +42 -0
- data/sig/bidi2pdf/bidi/client.rbs +72 -0
- data/sig/bidi2pdf/bidi/event_manager.rbs +29 -0
- data/sig/bidi2pdf/bidi/network_event.rbs +51 -0
- data/sig/bidi2pdf/bidi/network_events.rbs +55 -0
- data/sig/bidi2pdf/bidi/print_parameters_validator.rbs +44 -0
- data/sig/bidi2pdf/bidi/session.rbs +52 -0
- data/sig/bidi2pdf/bidi/user_context.rbs +50 -0
- data/sig/bidi2pdf/bidi/web_socket_dispatcher.rbs +53 -0
- data/sig/bidi2pdf/chromedriver_manager.rbs +42 -0
- data/sig/bidi2pdf/cli.rbs +21 -0
- data/sig/bidi2pdf/launcher.rbs +38 -0
- data/sig/bidi2pdf/process_tree.rbs +27 -0
- data/sig/bidi2pdf/session_runner.rbs +51 -0
- data/sig/bidi2pdf/utils.rbs +5 -0
- data/sig/vendor/thor.rbs +13 -0
- data/tasks/changelog.rake +29 -0
- data/tasks/generate_rbs.rake +64 -0
- metadata +48 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dbc473cb8b54517914bfa958b1925d97db54ab3e6ee640a28101b48346077192
|
4
|
+
data.tar.gz: 76c2d11f399a33a932f66348cfc3aad9d0c80f1e15407566b10d16a646b17d1c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: eb72b6bc4eeb2a65172c37138be3f2af5129a046a217acaf90236532c323547e0f1456f7743c509015808389da586b145cd069a26a85bbc35278c44cfea32249
|
7
|
+
data.tar.gz: e3b6d68003a6419e86fa33a68b51c065d7d60f95cc7bf657b12cb13ed5bef085938599b5387aca641b1a699ec47fab6804e300892b962a842d06748b061a1e5d
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,8 +1,34 @@
|
|
1
|
-
|
1
|
+
<!-- generated by git-cliff start -->
|
2
|
+
|
3
|
+
# Changelog
|
4
|
+
|
5
|
+
All notable changes to this project will be documented in this file.
|
6
|
+
|
7
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
8
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
9
|
+
<!-- generated by git-cliff end -->
|
10
|
+
|
11
|
+
## [0.1.2] - 2025-04-05
|
12
|
+
|
13
|
+
### 🔄 Changed
|
14
|
+
|
15
|
+
- Enhance CLI with YAML configuration support and additional options by @dieter-medium
|
16
|
+
|
17
|
+
### 🚀 Added
|
18
|
+
|
19
|
+
- Add CLI tests for rendering and configuration options by @dieter-medium
|
20
|
+
- Add RSpec test for Bidi2pdf version number by @dieter-medium
|
21
|
+
- Add PDF rendering test with custom print parameters and debug output by @dieter-medium
|
22
|
+
- Add be_alive_process matcher and chromedriver manager specs for process management by @dieter-medium
|
23
|
+
- Add RBS type signatures for Bidi2pdf classes and modules by @dieter-medium
|
24
|
+
- Add build status badge to README by @dieter-medium
|
25
|
+
- Add badges for maintainability, gem version, and test coverage in README by @dieter-medium
|
26
|
+
- Add Code Climate coverage reporting to CI workflow by @dieter-medium
|
27
|
+
- Add support for remote ChromeDriver connections and update Docker setup by @dieter-medium
|
2
28
|
|
3
29
|
## [0.1.1] - 2025-04-01
|
4
30
|
|
5
|
-
### Added
|
31
|
+
### 🚀 Added
|
6
32
|
|
7
33
|
- Docker Compose setup with Nginx for authentication examples
|
8
34
|
- Sample configurations for different authentication methods:
|
@@ -10,7 +36,7 @@
|
|
10
36
|
- Cookie-based authentication
|
11
37
|
- API key header authentication
|
12
38
|
|
13
|
-
### Fixed
|
39
|
+
### 🐛 Fixed
|
14
40
|
|
15
41
|
- HTTP cookie handling issues
|
16
42
|
|
data/README.md
CHANGED
@@ -1,3 +1,8 @@
|
|
1
|
+
[](https://github.com/dieter-medium/bidi2pdf/blob/main/.github/workflows/ruby.yml)
|
2
|
+
[](https://codeclimate.com/github/dieter-medium/bidi2pdf/maintainability)
|
3
|
+
[](https://badge.fury.io/rb/bidi2pdf)
|
4
|
+
[](https://codeclimate.com/github/dieter-medium/bidi2pdf/test_coverage)
|
5
|
+
|
1
6
|
# Bidi2pdf
|
2
7
|
|
3
8
|
Bidi2pdf is a Ruby gem that generates high-quality PDFs from web pages using Chrome's BiDi (BiDirectional) protocol. It
|
@@ -94,6 +99,7 @@ docker run -it --rm -v ./output:/reports bidi2pdf \
|
|
94
99
|
### Test it with docker compose
|
95
100
|
|
96
101
|
```bash
|
102
|
+
rake build
|
97
103
|
docker compose -f docker/docker-compose.yml up -d
|
98
104
|
|
99
105
|
# simple example
|
@@ -108,6 +114,8 @@ docker compose -f docker/docker-compose.yml exec app bidi2pdf render --url=http:
|
|
108
114
|
# cookie example
|
109
115
|
docker compose -f docker/docker-compose.yml exec app bidi2pdf render --url=http://nginx/cookie/sample.html --cookie "auth=secret" --wait_window_loaded --wait_network_idle --output /reports/cookie.pdf
|
110
116
|
|
117
|
+
# remote chrome example
|
118
|
+
docker compose -f docker/docker-compose.yml exec app bidi2pdf render --url=http://nginx/cookie/sample.html --remote_browser_url http://remote-chrome:3000/session --cookie "auth=secret" --wait_window_loaded --wait_network_idle --output /reports/remote.pdf
|
111
119
|
|
112
120
|
docker compose -f docker/docker-compose.yml down
|
113
121
|
```
|
@@ -126,6 +134,8 @@ docker compose -f docker/docker-compose.yml down
|
|
126
134
|
| `--wait_window_loaded` | Wait for the window to be fully loaded. You need to set a variable `window.loaded`. See ./spec/fixtures/sample.html |
|
127
135
|
| `--wait_network_idle` | Wait for network to be idle |
|
128
136
|
| `--log_level` | Log level (debug, info, warn, error, fatal) |
|
137
|
+
| `--remote_browser_url` | URL of the remote Chrome instance (default: nil) |
|
138
|
+
| `--default_timeout` | Default timeout for operations (default: 60 seconds) |
|
129
139
|
|
130
140
|
## Development
|
131
141
|
|
data/Rakefile
CHANGED
data/cliff.toml
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
# git-cliff ~ configuration file
|
2
|
+
# https://git-cliff.org/docs/configuration
|
3
|
+
|
4
|
+
[changelog]
|
5
|
+
# template for the changelog header
|
6
|
+
header = """
|
7
|
+
<!-- generated by git-cliff start -->
|
8
|
+
# Changelog\n
|
9
|
+
All notable changes to this project will be documented in this file.
|
10
|
+
|
11
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
12
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).\n
|
13
|
+
"""
|
14
|
+
# template for the changelog body
|
15
|
+
# https://keats.github.io/tera/docs/#introduction
|
16
|
+
body = """
|
17
|
+
{%- macro remote_url() -%}
|
18
|
+
https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
|
19
|
+
{%- endmacro -%}
|
20
|
+
|
21
|
+
{% if version -%}
|
22
|
+
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
|
23
|
+
{% else -%}
|
24
|
+
## [Unreleased]
|
25
|
+
{% endif -%}
|
26
|
+
|
27
|
+
{% for group, commits in commits | group_by(attribute="group") %}
|
28
|
+
### {{ group | upper_first }}
|
29
|
+
{%- for commit in commits %}
|
30
|
+
- {{ commit.message | split(pat="\n") | first | upper_first | trim }}\
|
31
|
+
{% if commit.remote.username %} by @{{ commit.remote.username }}{%- endif -%}
|
32
|
+
{% if commit.remote.pr_number %} in \
|
33
|
+
[#{{ commit.remote.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.remote.pr_number }}) \
|
34
|
+
{%- endif -%}
|
35
|
+
{% endfor %}
|
36
|
+
{% endfor %}
|
37
|
+
|
38
|
+
{%- if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
|
39
|
+
## New Contributors
|
40
|
+
{%- endif -%}
|
41
|
+
|
42
|
+
{% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}
|
43
|
+
* @{{ contributor.username }} made their first contribution
|
44
|
+
{%- if contributor.pr_number %} in \
|
45
|
+
[#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \
|
46
|
+
{%- endif %}
|
47
|
+
{%- endfor %}\n
|
48
|
+
"""
|
49
|
+
# template for the changelog footer
|
50
|
+
footer = """
|
51
|
+
{%- macro remote_url() -%}
|
52
|
+
https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
|
53
|
+
{%- endmacro -%}
|
54
|
+
|
55
|
+
{% for release in releases -%}
|
56
|
+
{% if release.version -%}
|
57
|
+
{% if release.previous.version -%}
|
58
|
+
[{{ release.version | trim_start_matches(pat="v") }}]: \
|
59
|
+
{{ self::remote_url() }}/compare/{{ release.previous.version }}..{{ release.version }}
|
60
|
+
{% endif -%}
|
61
|
+
{% else -%}
|
62
|
+
[unreleased]: {{ self::remote_url() }}/compare/{{ release.previous.version }}..HEAD
|
63
|
+
{% endif -%}
|
64
|
+
{% endfor %}
|
65
|
+
<!-- generated by git-cliff end -->
|
66
|
+
"""
|
67
|
+
# remove the leading and trailing whitespace from the templates
|
68
|
+
trim = true
|
69
|
+
|
70
|
+
[git]
|
71
|
+
# parse the commits based on https://www.conventionalcommits.org
|
72
|
+
conventional_commits = true
|
73
|
+
# filter out the commits that are not conventional
|
74
|
+
filter_unconventional = false
|
75
|
+
# regex for preprocessing the commit messages
|
76
|
+
commit_preprocessors = [
|
77
|
+
# remove issue numbers from commits
|
78
|
+
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" },
|
79
|
+
]
|
80
|
+
# regex for parsing and grouping commits
|
81
|
+
commit_parsers = [
|
82
|
+
# ✅ Additions
|
83
|
+
{ message = "^[a|A]dd", group = "🚀 Added" },
|
84
|
+
{ message = "^[s|S]upport", group = "🚀 Added" },
|
85
|
+
{ message = "^.*: add", group = "🚀 Added" },
|
86
|
+
{ message = "^.*: support", group = "🚀 Added" },
|
87
|
+
|
88
|
+
# ❌ Removals
|
89
|
+
{ message = "^[r|R]emove", group = "🗑️ Removed" },
|
90
|
+
{ message = "^.*: remove", group = "🗑️ Removed" },
|
91
|
+
{ message = "^.*: delete", group = "🗑️ Removed" },
|
92
|
+
|
93
|
+
# 🐛 Fixes
|
94
|
+
{ message = "^test", group = "🐛 Fixed" },
|
95
|
+
{ message = "^fix", group = "🐛 Fixed" },
|
96
|
+
{ message = "^.*: fix", group = "🐛 Fixed" },
|
97
|
+
|
98
|
+
# ⛔️ Chore & CI commits to skip
|
99
|
+
{ message = "^chore\\(release\\): prepare for", skip = true },
|
100
|
+
{ message = "^chore\\(deps.*\\)", skip = true },
|
101
|
+
{ message = "^chore\\(pr\\)", skip = true },
|
102
|
+
{ message = "^chore\\(pull\\)", skip = true },
|
103
|
+
{ message = "^chore", skip = true },
|
104
|
+
{ message = "^ci", skip = true },
|
105
|
+
|
106
|
+
# 🌀 Catch-All
|
107
|
+
{ message = "^.*", group = "🔄 Changed" }
|
108
|
+
]
|
109
|
+
# filter out the commits that are not matched by commit parsers
|
110
|
+
filter_commits = false
|
111
|
+
# sort the tags topologically
|
112
|
+
topo_order = false
|
113
|
+
# sort the commits inside sections by oldest/newest order
|
114
|
+
sort_commits = "newest"
|
@@ -0,0 +1,33 @@
|
|
1
|
+
FROM ruby:3.3
|
2
|
+
|
3
|
+
# Install dependencies
|
4
|
+
RUN apt-get update && \
|
5
|
+
apt-get install -y \
|
6
|
+
chromium \
|
7
|
+
libglib2.0-0 \
|
8
|
+
libnss3 \
|
9
|
+
libxss1 \
|
10
|
+
libasound2 \
|
11
|
+
libatk-bridge2.0-0 \
|
12
|
+
libgtk-3-0 \
|
13
|
+
libdrm2 \
|
14
|
+
curl \
|
15
|
+
unzip \
|
16
|
+
xvfb \
|
17
|
+
&& rm -rf /var/lib/apt/lists/*
|
18
|
+
|
19
|
+
# Create a non-root user
|
20
|
+
RUN groupadd -r appuser && useradd -r -g appuser -m -d /home/appuser appuser
|
21
|
+
|
22
|
+
COPY ./docker/entrypoint.sh /usr/local/bin/entrypoint.sh
|
23
|
+
RUN chmod +x /usr/local/bin/entrypoint.sh
|
24
|
+
|
25
|
+
# Set working directory
|
26
|
+
WORKDIR /app
|
27
|
+
|
28
|
+
# Switch to non-root user
|
29
|
+
USER appuser
|
30
|
+
|
31
|
+
RUN gem install chromedriver-binary && ruby -e 'require "chromedriver/binary"; puts Chromedriver::Binary::ChromedriverDownloader.update'
|
32
|
+
|
33
|
+
CMD ["/usr/local/bin/entrypoint.sh"]
|
data/docker/docker-compose.yml
CHANGED
data/lib/bidi2pdf/bidi/client.rb
CHANGED
@@ -6,6 +6,8 @@ require "websocket-client-simple"
|
|
6
6
|
require_relative "web_socket_dispatcher"
|
7
7
|
require_relative "add_headers_interceptor"
|
8
8
|
require_relative "auth_interceptor"
|
9
|
+
require_relative "command_manager"
|
10
|
+
require_relative "connection_manager"
|
9
11
|
|
10
12
|
module Bidi2pdf
|
11
13
|
module Bidi
|
@@ -21,7 +23,7 @@ module Bidi2pdf
|
|
21
23
|
|
22
24
|
@connected = false
|
23
25
|
@connection_mutex = Mutex.new
|
24
|
-
@
|
26
|
+
@next_id_mutex = Mutex.new
|
25
27
|
@connection_cv = ConditionVariable.new
|
26
28
|
|
27
29
|
@started = false
|
@@ -31,192 +33,105 @@ module Bidi2pdf
|
|
31
33
|
return @socket if started?
|
32
34
|
|
33
35
|
@socket = WebSocket::Client::Simple.connect(ws_url)
|
34
|
-
@dispatcher = WebSocketDispatcher.new(@socket)
|
35
36
|
|
36
|
-
@
|
37
|
-
@
|
37
|
+
@connection_manager = ConnectionManager.new(logger: Bidi2pdf.logger)
|
38
|
+
@command_manager = CommandManager.new(@socket, logger: Bidi2pdf.logger)
|
38
39
|
|
39
|
-
|
40
|
+
dispatcher.on_open { @connection_manager.mark_connected }
|
41
|
+
dispatcher.on_message { |data| handle_response_to_cmd(data) }
|
40
42
|
|
43
|
+
dispatcher.start_listening
|
41
44
|
@started = true
|
42
45
|
|
43
46
|
@socket
|
44
47
|
end
|
45
48
|
|
46
|
-
def started?
|
47
|
-
@started
|
48
|
-
end
|
49
|
+
def started? = @started
|
49
50
|
|
50
51
|
def wait_until_open(timeout: Bidi2pdf.default_timeout)
|
51
|
-
@
|
52
|
-
unless @connected
|
53
|
-
Bidi2pdf.logger.debug "Waiting for WebSocket connection to open"
|
54
|
-
@connection_cv.wait(@connection_mutex, timeout)
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
raise "WebSocket connection did not open in time" unless @connected
|
59
|
-
|
60
|
-
Bidi2pdf.logger.debug "WebSocket connection is open"
|
52
|
+
@connection_manager.wait_until_open(timeout: timeout)
|
61
53
|
end
|
62
54
|
|
63
55
|
def send_cmd(method, params = {})
|
64
|
-
|
65
|
-
payload = {
|
66
|
-
id: cmd_id,
|
67
|
-
method: method,
|
68
|
-
params: params
|
69
|
-
}
|
70
|
-
|
71
|
-
Bidi2pdf.logger.debug "Sending command: #{redact_sensitive_fields(payload).inspect}"
|
72
|
-
|
73
|
-
@socket.send(payload.to_json)
|
74
|
-
end
|
56
|
+
@command_manager.send_cmd(method, params)
|
75
57
|
end
|
76
58
|
|
77
|
-
|
78
|
-
def send_cmd_and_wait(method, params = {}, timeout: Bidi2pdf.default_timeout)
|
59
|
+
def send_cmd_and_wait(method, params = {}, timeout: Bidi2pdf.default_timeout, &block)
|
79
60
|
timed("Command #{method}") do
|
80
|
-
|
81
|
-
queue = @pending_responses[id]
|
82
|
-
|
83
|
-
begin
|
84
|
-
response = queue.pop(true)
|
85
|
-
rescue ThreadError
|
86
|
-
response = queue.pop(timeout: timeout)
|
87
|
-
end
|
88
|
-
|
89
|
-
if response.nil?
|
90
|
-
# rubocop:disable Layout/LineLength
|
91
|
-
Bidi2pdf.logger.error "Timeout waiting for response to command #{id}, cmd: #{method}, params: #{redact_sensitive_fields(params).inspect}"
|
92
|
-
# rubocop:enable Layout/LineLength
|
93
|
-
|
94
|
-
raise "Timeout waiting for response to command ID #{id}"
|
95
|
-
end
|
96
|
-
|
97
|
-
raise "Error response: #{response["error"]}" if response["error"]
|
98
|
-
|
99
|
-
result = response
|
100
|
-
|
101
|
-
result = yield response if block_given?
|
102
|
-
|
103
|
-
result
|
104
|
-
ensure
|
105
|
-
@pending_responses.delete(id)
|
61
|
+
@command_manager.send_cmd_and_wait(method, params, timeout: timeout, &block)
|
106
62
|
end
|
107
63
|
end
|
108
64
|
|
109
|
-
|
65
|
+
def on_message(&block) = dispatcher.on_message(&block)
|
110
66
|
|
111
|
-
|
112
|
-
def on_message(&block) = @dispatcher.on_message(&block)
|
67
|
+
def on_open(&block) = dispatcher.on_open(&block)
|
113
68
|
|
114
|
-
def
|
69
|
+
def on_close(&block) = dispatcher.on_close(&block)
|
115
70
|
|
116
|
-
def
|
117
|
-
|
118
|
-
def on_error(&block) = @dispatcher.on_error(&block)
|
71
|
+
def on_error(&block) = dispatcher.on_error(&block)
|
119
72
|
|
120
73
|
def on_event(*names, &block)
|
121
|
-
names.each
|
122
|
-
|
123
|
-
end
|
124
|
-
|
125
|
-
send_cmd "session.subscribe", { events: names } if names.any?
|
74
|
+
names.each { |name| dispatcher.on_event(name, &block) }
|
75
|
+
send_cmd("session.subscribe", { events: names }) if names.any?
|
126
76
|
end
|
127
77
|
|
128
|
-
def remove_message_listener(block) =
|
78
|
+
def remove_message_listener(block) = dispatcher.remove_message_listener(block)
|
129
79
|
|
130
80
|
def remove_event_listener(*names, &block)
|
131
|
-
names.each
|
132
|
-
@dispatcher.remove_event_listener(event_name, block)
|
133
|
-
end
|
81
|
+
names.each { |event_name| dispatcher.remove_event_listener(event_name, block) }
|
134
82
|
end
|
135
83
|
|
136
|
-
def add_headers_interceptor(
|
137
|
-
|
138
|
-
url_patterns:,
|
139
|
-
headers:
|
140
|
-
)
|
141
|
-
send_cmd_and_wait("network.addIntercept", {
|
84
|
+
def add_headers_interceptor(context:, url_patterns:, headers:)
|
85
|
+
add_interceptor(
|
142
86
|
context: context,
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
AddHeadersInterceptor.new(id, headers, self).tap do |interceptor|
|
150
|
-
on_event "network.beforeRequestSent", &interceptor.method(:handle_event)
|
151
|
-
end
|
152
|
-
end
|
87
|
+
url_patterns: url_patterns,
|
88
|
+
phase: "beforeRequestSent",
|
89
|
+
event: "network.beforeRequestSent",
|
90
|
+
interceptor_class: AddHeadersInterceptor,
|
91
|
+
extra_args: { headers: headers }
|
92
|
+
)
|
153
93
|
end
|
154
94
|
|
155
|
-
def add_auth_interceptor(
|
156
|
-
|
157
|
-
url_patterns:,
|
158
|
-
username:,
|
159
|
-
password:
|
160
|
-
)
|
161
|
-
send_cmd_and_wait("network.addIntercept", {
|
95
|
+
def add_auth_interceptor(context:, url_patterns:, username:, password:)
|
96
|
+
add_interceptor(
|
162
97
|
context: context,
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
AuthInterceptor.new(id, username, password, self).tap do |interceptor|
|
170
|
-
on_event "network.authRequired", &interceptor.method(:handle_event)
|
171
|
-
end
|
172
|
-
end
|
98
|
+
url_patterns: url_patterns,
|
99
|
+
phase: "authRequired",
|
100
|
+
event: "network.authRequired",
|
101
|
+
interceptor_class: AuthInterceptor,
|
102
|
+
extra_args: { username: username, password: password }
|
103
|
+
)
|
173
104
|
end
|
174
105
|
|
175
106
|
private
|
176
107
|
|
177
|
-
def
|
178
|
-
|
179
|
-
|
180
|
-
@send_cmd_mutex.synchronize do
|
181
|
-
@id += 1
|
182
|
-
cmd_id = @id
|
183
|
-
@pending_responses[cmd_id] = Queue.new
|
184
|
-
end
|
185
|
-
|
186
|
-
cmd_id
|
187
|
-
end
|
188
|
-
|
189
|
-
def handle_open
|
190
|
-
@connection_mutex.synchronize do
|
191
|
-
@connected = true
|
192
|
-
@connection_cv.broadcast
|
193
|
-
end
|
108
|
+
def dispatcher
|
109
|
+
@dispatcher ||= WebSocketDispatcher.new(@socket)
|
194
110
|
end
|
195
111
|
|
196
112
|
def handle_response_to_cmd(data)
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
113
|
+
handled = @command_manager.handle_response(data)
|
114
|
+
return if handled
|
115
|
+
|
116
|
+
if data["error"]
|
117
|
+
Bidi2pdf.logger.error "Error response: #{data["error"].inspect}"
|
201
118
|
else
|
202
119
|
Bidi2pdf.logger.warn "Unknown response: #{data.inspect}"
|
203
120
|
end
|
204
121
|
end
|
205
122
|
|
206
|
-
def
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
123
|
+
def add_interceptor(context:, url_patterns:, phase:, event:, interceptor_class:, extra_args: {})
|
124
|
+
send_cmd_and_wait("network.addIntercept", {
|
125
|
+
context: context,
|
126
|
+
phases: [phase],
|
127
|
+
urlPatterns: url_patterns
|
128
|
+
}) do |response|
|
129
|
+
id = response["result"]["intercept"]
|
130
|
+
Bidi2pdf.logger.debug "Interceptor added: #{id}"
|
131
|
+
|
132
|
+
interceptor_class.new(id, **extra_args, client: self).tap do |interceptor|
|
133
|
+
on_event(event, &interceptor.method(:handle_event))
|
215
134
|
end
|
216
|
-
when Array
|
217
|
-
obj.map { |item| redact_sensitive_fields(item, sensitive_keys) }
|
218
|
-
else
|
219
|
-
obj
|
220
135
|
end
|
221
136
|
end
|
222
137
|
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bidi2pdf
|
4
|
+
module Bidi
|
5
|
+
class CommandManager
|
6
|
+
def initialize(socket, logger:)
|
7
|
+
@socket = socket
|
8
|
+
@logger = logger
|
9
|
+
|
10
|
+
@id = 0
|
11
|
+
@next_id_mutex = Mutex.new
|
12
|
+
@pending_responses = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
def send_cmd(method, params = {})
|
16
|
+
id = next_id
|
17
|
+
payload = { id: id, method: method, params: params }
|
18
|
+
|
19
|
+
@logger.debug "Sending command: #{redact_sensitive_fields(payload).inspect}"
|
20
|
+
@socket.send(payload.to_json)
|
21
|
+
|
22
|
+
id
|
23
|
+
end
|
24
|
+
|
25
|
+
def send_cmd_and_wait(method, params = {}, timeout: Bidi2pdf.default_timeout)
|
26
|
+
id = send_cmd(method, params)
|
27
|
+
queue = @pending_responses[id]
|
28
|
+
|
29
|
+
response = queue.pop(timeout: timeout)
|
30
|
+
raise_timeout_error(id, method, params) if response.nil?
|
31
|
+
raise "Error response: #{response["error"]}" if response["error"]
|
32
|
+
|
33
|
+
block_given? ? yield(response) : response
|
34
|
+
ensure
|
35
|
+
@pending_responses.delete(id)
|
36
|
+
end
|
37
|
+
|
38
|
+
def queue_for(id)
|
39
|
+
@pending_responses[id]
|
40
|
+
end
|
41
|
+
|
42
|
+
def handle_response(data)
|
43
|
+
if (id = data["id"]) && @pending_responses.key?(id)
|
44
|
+
@pending_responses[id]&.push(data)
|
45
|
+
else
|
46
|
+
false
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def next_id
|
53
|
+
@next_id_mutex.synchronize do
|
54
|
+
@id += 1
|
55
|
+
@pending_responses[@id] = Thread::Queue.new
|
56
|
+
@id
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def redact_sensitive_fields(obj, sensitive_keys = %w[value token password authorization username])
|
61
|
+
case obj
|
62
|
+
when Hash
|
63
|
+
obj.transform_values.with_index do |v, idx|
|
64
|
+
k = obj.keys[idx]
|
65
|
+
sensitive_keys.include?(k.to_s.downcase) ? "[REDACTED]" : redact_sensitive_fields(v, sensitive_keys)
|
66
|
+
end
|
67
|
+
when Array
|
68
|
+
obj.map { |item| redact_sensitive_fields(item, sensitive_keys) }
|
69
|
+
else
|
70
|
+
obj
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def raise_timeout_error(id, method, params)
|
75
|
+
# rubocop:disable Layout/LineLength
|
76
|
+
@logger.error "Timeout waiting for response to command #{id}, cmd: #{method}, params: #{redact_sensitive_fields(params).inspect}"
|
77
|
+
# rubocop:enable Layout/LineLength
|
78
|
+
raise "Timeout waiting for response to command ID #{id}"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|