bidi2pdf 0.1.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.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/.idea/.gitignore +8 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +50 -0
  5. data/.ruby-gemset +1 -0
  6. data/.ruby-version +1 -0
  7. data/CHANGELOG.md +5 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +119 -0
  10. data/Rakefile +22 -0
  11. data/docker/Dockerfile +35 -0
  12. data/docker/docker-compose.yml +1 -0
  13. data/exe/bidi2pdf +7 -0
  14. data/lib/bidi2pdf/bidi/add_headers_interceptor.rb +42 -0
  15. data/lib/bidi2pdf/bidi/auth_interceptor.rb +67 -0
  16. data/lib/bidi2pdf/bidi/browser.rb +15 -0
  17. data/lib/bidi2pdf/bidi/browser_tab.rb +180 -0
  18. data/lib/bidi2pdf/bidi/client.rb +224 -0
  19. data/lib/bidi2pdf/bidi/event_manager.rb +84 -0
  20. data/lib/bidi2pdf/bidi/network_event.rb +54 -0
  21. data/lib/bidi2pdf/bidi/network_events.rb +82 -0
  22. data/lib/bidi2pdf/bidi/print_parameters_validator.rb +114 -0
  23. data/lib/bidi2pdf/bidi/session.rb +135 -0
  24. data/lib/bidi2pdf/bidi/user_context.rb +75 -0
  25. data/lib/bidi2pdf/bidi/web_socket_dispatcher.rb +70 -0
  26. data/lib/bidi2pdf/chromedriver_manager.rb +160 -0
  27. data/lib/bidi2pdf/cli.rb +118 -0
  28. data/lib/bidi2pdf/launcher.rb +46 -0
  29. data/lib/bidi2pdf/session_runner.rb +123 -0
  30. data/lib/bidi2pdf/utils.rb +15 -0
  31. data/lib/bidi2pdf/version.rb +5 -0
  32. data/lib/bidi2pdf.rb +25 -0
  33. data/sig/bidi2pdf/chrome/chromedriver_downloader.rbs +11 -0
  34. data/sig/bidi2pdf/chrome/downloader_helper.rbs +9 -0
  35. data/sig/bidi2pdf/chrome/finder.rbs +27 -0
  36. data/sig/bidi2pdf/chrome/platform.rbs +13 -0
  37. data/sig/bidi2pdf/chrome/version_resolver.rbs +19 -0
  38. data/sig/bidi2pdf.rbs +4 -0
  39. data/tmp/.keep +0 -0
  40. metadata +327 -0
@@ -0,0 +1,224 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "websocket-client-simple"
5
+
6
+ require_relative "web_socket_dispatcher"
7
+ require_relative "add_headers_interceptor"
8
+ require_relative "auth_interceptor"
9
+
10
+ module Bidi2pdf
11
+ module Bidi
12
+ class Client
13
+ include Bidi2pdf::Utils
14
+
15
+ attr_reader :ws_url
16
+
17
+ def initialize(ws_url)
18
+ @ws_url = ws_url
19
+ @id = 0
20
+ @pending_responses = {}
21
+
22
+ @connected = false
23
+ @connection_mutex = Mutex.new
24
+ @send_cmd_mutex = Mutex.new
25
+ @connection_cv = ConditionVariable.new
26
+
27
+ @started = false
28
+ end
29
+
30
+ def start
31
+ return @socket if started?
32
+
33
+ @socket = WebSocket::Client::Simple.connect(ws_url)
34
+ @dispatcher = WebSocketDispatcher.new(@socket)
35
+
36
+ @dispatcher.on_open { handle_open }
37
+ @dispatcher.on_message { |data| handle_response_to_cmd(data) }
38
+
39
+ @dispatcher.start_listening
40
+
41
+ @started = true
42
+
43
+ @socket
44
+ end
45
+
46
+ def started?
47
+ @started
48
+ end
49
+
50
+ def wait_until_open(timeout: Bidi2pdf.default_timeout)
51
+ @connection_mutex.synchronize do
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"
61
+ end
62
+
63
+ def send_cmd(method, params = {})
64
+ next_id.tap do |cmd_id|
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
75
+ end
76
+
77
+ # rubocop:disable Metrics/AbcSize
78
+ def send_cmd_and_wait(method, params = {}, timeout: Bidi2pdf.default_timeout)
79
+ timed("Command #{method}") do
80
+ id = send_cmd(method, params)
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)
106
+ end
107
+ end
108
+
109
+ # rubocop:enable Metrics/AbcSize
110
+
111
+ # Event API for external consumers
112
+ def on_message(&block) = @dispatcher.on_message(&block)
113
+
114
+ def on_open(&block) = @dispatcher.on_open(&block)
115
+
116
+ def on_close(&block) = @dispatcher.on_close(&block)
117
+
118
+ def on_error(&block) = @dispatcher.on_error(&block)
119
+
120
+ def on_event(*names, &block)
121
+ names.each do |name|
122
+ @dispatcher.on_event(name, &block)
123
+ end
124
+
125
+ send_cmd "session.subscribe", { events: names } if names.any?
126
+ end
127
+
128
+ def remove_message_listener(block) = @dispatcher.remove_message_listener(block)
129
+
130
+ def remove_event_listener(*names, &block)
131
+ names.each do |event_name|
132
+ @dispatcher.remove_event_listener(event_name, block)
133
+ end
134
+ end
135
+
136
+ def add_headers_interceptor(
137
+ context:,
138
+ url_patterns:,
139
+ headers:
140
+ )
141
+ send_cmd_and_wait("network.addIntercept", {
142
+ context: context,
143
+ phases: ["beforeRequestSent"],
144
+ urlPatterns: url_patterns
145
+ }) do |response|
146
+ id = response["result"]["intercept"]
147
+ Bidi2pdf.logger.debug "Interceptor added: #{id}"
148
+
149
+ AddHeadersInterceptor.new(id, headers, self).tap do |interceptor|
150
+ on_event "network.beforeRequestSent", &interceptor.method(:handle_event)
151
+ end
152
+ end
153
+ end
154
+
155
+ def add_auth_interceptor(
156
+ context:,
157
+ url_patterns:,
158
+ username:,
159
+ password:
160
+ )
161
+ send_cmd_and_wait("network.addIntercept", {
162
+ context: context,
163
+ phases: ["authRequired"],
164
+ urlPatterns: url_patterns
165
+ }) do |response|
166
+ id = response["result"]["intercept"]
167
+ Bidi2pdf.logger.debug "Interceptor added: #{id}"
168
+
169
+ AuthInterceptor.new(id, username, password, self).tap do |interceptor|
170
+ on_event "network.authRequired", &interceptor.method(:handle_event)
171
+ end
172
+ end
173
+ end
174
+
175
+ private
176
+
177
+ def next_id
178
+ cmd_id = nil
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
194
+ end
195
+
196
+ def handle_response_to_cmd(data)
197
+ if (id = data["id"]) && @pending_responses.key?(id)
198
+ @pending_responses[id]&.push(data)
199
+ elsif (data = data["error"])
200
+ Bidi2pdf.logger.error "Error response: #{data.inspect}"
201
+ else
202
+ Bidi2pdf.logger.warn "Unknown response: #{data.inspect}"
203
+ end
204
+ end
205
+
206
+ def redact_sensitive_fields(obj, sensitive_keys = %w[value token password authorization username])
207
+ case obj
208
+ when Hash
209
+ obj.each_with_object({}) do |(k, v), result|
210
+ result[k] = if sensitive_keys.include?(k.to_s.downcase)
211
+ "[REDACTED]"
212
+ else
213
+ redact_sensitive_fields(v, sensitive_keys)
214
+ end
215
+ end
216
+ when Array
217
+ obj.map { |item| redact_sensitive_fields(item, sensitive_keys) }
218
+ else
219
+ obj
220
+ end
221
+ end
222
+ end
223
+ end
224
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bidi2pdf
4
+ module Bidi
5
+ class EventManager
6
+ attr_reader :type
7
+
8
+ def initialize(type)
9
+ @listeners = Hash.new { |h, k| h[k] = [] }
10
+ @type = type
11
+ end
12
+
13
+ def on(*event_names, &block)
14
+ event_names.each { |event_name| @listeners[event_name.to_sym] << block }
15
+
16
+ block
17
+ end
18
+
19
+ def off(event_name, block) = @listeners[event_name.to_sym].delete(block)
20
+
21
+ def dispatch(event_name, *args)
22
+ listeners = @listeners[event_name.to_sym] || []
23
+
24
+ if event_name.to_s.include?(".")
25
+ toplevel_event_name = event_name.to_s.split(".").first
26
+ listeners += @listeners[toplevel_event_name.to_sym]
27
+ end
28
+
29
+ log_msg("Dispatching #{type} '#{event_name}' to #{listeners.size} listeners", args)
30
+
31
+ listeners.each { |listener| listener.call(*args) }
32
+ end
33
+
34
+ def clear(event_name = nil)
35
+ if event_name
36
+ @listeners[event_name].clear
37
+ else
38
+ @listeners.clear
39
+ end
40
+ end
41
+
42
+ def log_msg(prefix, data)
43
+ message = truncate_large_values(data)
44
+ Bidi2pdf.logger.debug "#{prefix}: #{message.inspect}"
45
+ end
46
+
47
+ # rubocop: disable all
48
+ def truncate_large_values(org, max_length = 50, max_depth = 5, current_depth = 0)
49
+ return "...(too deep)..." if current_depth >= max_depth
50
+
51
+ obj = org.dup
52
+
53
+ case obj
54
+ when Hash
55
+ obj.each_with_object({}) do |(k, v), result|
56
+ result[k] = if %w[username password].include?(k.to_s.downcase)
57
+ "[REDACTED]"
58
+ else
59
+ truncate_large_values(v, max_length, max_depth, current_depth + 1)
60
+ end
61
+ end
62
+ when Array
63
+ if obj.size > 10
64
+ obj.take(10).map do |v|
65
+ truncate_large_values(v, max_length, max_depth, current_depth + 1)
66
+ end + ["...(#{obj.size - 10} more items)"]
67
+ else
68
+ obj.map { |v| truncate_large_values(v, max_length, max_depth, current_depth + 1) }
69
+ end
70
+ when String
71
+ if obj.length > max_length
72
+ "#{obj[0...max_length]}... (truncated, total length: #{obj.length})"
73
+ else
74
+ obj
75
+ end
76
+ else
77
+ obj
78
+ end
79
+ end
80
+
81
+ # rubocop: enable all
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bidi2pdf
4
+ module Bidi
5
+ class NetworkEvent
6
+ attr_reader :id, :url, :state, :start_timestamp, :end_timestamp, :timing
7
+
8
+ STATE_MAP = {
9
+ "network.responseStarted" => "started",
10
+ "network.responseCompleted" => "completed",
11
+ "network.fetchError" => "error"
12
+ }.freeze
13
+
14
+ def initialize(id:, url:, timestamp:, timing:, state:)
15
+ @id = id
16
+ @url = url
17
+ @start_timestamp = timestamp
18
+ @timing = timing
19
+ @state = map_state(state)
20
+ end
21
+
22
+ def update_state(new_state, timestamp: nil, timing: nil)
23
+ @state = map_state(new_state)
24
+ @end_timestamp = timestamp if timestamp
25
+ @timing = timing if timing
26
+ end
27
+
28
+ def map_state(state)
29
+ STATE_MAP.fetch(state, state)
30
+ end
31
+
32
+ def format_timestamp(timestamp)
33
+ return "N/A" unless timestamp
34
+
35
+ Time.at(timestamp / 1000.0).utc.strftime("%Y-%m-%d %H:%M:%S.%L UTC")
36
+ end
37
+
38
+ def duration_seconds
39
+ return nil unless @start_timestamp && @end_timestamp
40
+
41
+ ((@end_timestamp - @start_timestamp) / 1000.0).round(3)
42
+ end
43
+
44
+ def in_progress? = state == "started"
45
+
46
+ def to_s
47
+ took_str = duration_seconds ? "took #{duration_seconds} sec" : "in progress"
48
+ "#<NetworkEvent id=#{@id} url=#{@url} state=#{@state} " \
49
+ "start=#{format_timestamp(@start_timestamp)} " \
50
+ "end=#{format_timestamp(@end_timestamp)} #{took_str}>"
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "network_event"
4
+
5
+ module Bidi2pdf
6
+ module Bidi
7
+ class NetworkEvents
8
+ attr_reader :context_id, :events
9
+
10
+ def initialize(context_id)
11
+ @context_id = context_id
12
+ @events = {}
13
+ end
14
+
15
+ def handle_event(data)
16
+ event = data["params"]
17
+ method = data["method"]
18
+
19
+ if event["context"] == context_id
20
+ handle_response(method, event)
21
+ else
22
+ Bidi2pdf.logger.debug "Ignoring Network event: #{method}, #{context_id}, params: #{event}"
23
+ end
24
+ rescue StandardError => e
25
+ Bidi2pdf.logger.error "Error handling network event: #{e.message}"
26
+ end
27
+
28
+ # rubocop:disable Metrics/AbcSize
29
+ def handle_response(method, event)
30
+ return unless event && event["request"]
31
+
32
+ request = event["request"]
33
+
34
+ id = request["request"]
35
+ url = request["url"]
36
+ timing = request["timings"]
37
+
38
+ timestamp = event["timestamp"]
39
+
40
+ if method == "network.responseStarted"
41
+ events[id] ||= NetworkEvent.new(
42
+ id: id,
43
+ url: url,
44
+ timestamp: timestamp,
45
+ timing: timing,
46
+ state: method
47
+ )
48
+ elsif events.key?(id)
49
+ events[id].update_state(method, timestamp: timestamp, timing: timing)
50
+ else
51
+ Bidi2pdf.logger.warn "Received response for unknown request ID: #{id}, URL: #{url}"
52
+ end
53
+ end
54
+
55
+ # rubocop:enable Metrics/AbcSize
56
+
57
+ def all_events
58
+ events.values.sort_by(&:start_timestamp)
59
+ end
60
+
61
+ def wait_until_all_finished(timeout: 10, poll_interval: 0.1)
62
+ start_time = Time.now
63
+
64
+ loop do
65
+ unless events.values.any?(&:in_progress?)
66
+ Bidi2pdf.logger.debug "✅ All network events completed."
67
+ break
68
+ end
69
+
70
+ if Time.now - start_time > timeout
71
+ # rubocop:disable Layout/LineLength
72
+ Bidi2pdf.logger.warn "⏰ Timeout while waiting for network events to complete. Still in progress: #{in_progress.map(&:id)}"
73
+ # rubocop:enable Layout/LineLength
74
+ break
75
+ end
76
+
77
+ sleep(poll_interval)
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bidi2pdf
4
+ module Bidi
5
+ # Validates parameters for the BiDi method `browsingContext.print`.
6
+ #
7
+ # Allowed structure of the params hash:
8
+ #
9
+ # {
10
+ # background: Boolean (optional, default: false) – print background graphics,
11
+ # margin: {
12
+ # top: Float >= 0.0 (optional, default: 1.0),
13
+ # bottom: Float >= 0.0 (optional, default: 1.0),
14
+ # left: Float >= 0.0 (optional, default: 1.0),
15
+ # right: Float >= 0.0 (optional, default: 1.0)
16
+ # },
17
+ # orientation: "portrait" or "landscape" (optional, default: "portrait"),
18
+ # page: {
19
+ # width: Float >= 0.0352 (optional, default: 21.59),
20
+ # height: Float >= 0.0352 (optional, default: 27.94)
21
+ # },
22
+ # pageRanges: Array of Integers or Strings (optional),
23
+ # scale: Float between 0.1 and 2.0 (optional, default: 1.0),
24
+ # shrinkToFit: Boolean (optional, default: true)
25
+ # }
26
+ #
27
+ # This validator checks presence, types, allowed ranges, and values,
28
+ # and raises ArgumentError with a descriptive message if validation fails.
29
+ class PrintParametersValidator
30
+ def self.validate!(params)
31
+ new(params).validate!
32
+ end
33
+
34
+ def initialize(params)
35
+ @params = params
36
+ end
37
+
38
+ def validate!
39
+ raise ArgumentError, "params must be a Hash" unless @params.is_a?(Hash)
40
+
41
+ validate_boolean(:background)
42
+ validate_boolean(:shrinkToFit)
43
+ validate_orientation
44
+ validate_scale
45
+ validate_page_ranges
46
+ validate_margin
47
+ validate_page_size
48
+
49
+ true
50
+ end
51
+
52
+ private
53
+
54
+ def validate_boolean(key)
55
+ return unless @params.key?(key)
56
+ return if [true, false].include?(@params[key])
57
+
58
+ raise ArgumentError, ":#{key} must be a boolean"
59
+ end
60
+
61
+ def validate_orientation
62
+ return unless @params.key?(:orientation)
63
+ return if %w[portrait landscape].include?(@params[:orientation])
64
+
65
+ raise ArgumentError, ":orientation must be 'portrait' or 'landscape'"
66
+ end
67
+
68
+ def validate_scale
69
+ return unless @params.key?(:scale)
70
+
71
+ scale = @params[:scale]
72
+ return if scale.is_a?(Numeric) && scale >= 0.1 && scale <= 2.0
73
+
74
+ raise ArgumentError, ":scale must be a number between 0.1 and 2.0"
75
+ end
76
+
77
+ def validate_page_ranges
78
+ return unless @params.key?(:pageRanges)
79
+ unless @params[:pageRanges].is_a?(Array) &&
80
+ @params[:pageRanges].all? { |v| v.is_a?(Integer) || v.is_a?(String) }
81
+ raise ArgumentError, ":pageRanges must be an array of integers or strings"
82
+ end
83
+ end
84
+
85
+ def validate_margin
86
+ return unless @params.key?(:margin)
87
+
88
+ margin = @params[:margin]
89
+ raise ArgumentError, ":margin must be a Hash" unless margin.is_a?(Hash)
90
+
91
+ %i[top bottom left right].each do |side|
92
+ next unless margin.key?(side)
93
+
94
+ val = margin[side]
95
+ raise ArgumentError, "margin[:#{side}] must be a float >= 0.0" unless val.is_a?(Numeric) && val >= 0.0
96
+ end
97
+ end
98
+
99
+ def validate_page_size
100
+ return unless @params.key?(:page)
101
+
102
+ page = @params[:page]
103
+ raise ArgumentError, ":page must be a Hash" unless page.is_a?(Hash)
104
+
105
+ %i[width height].each do |dim|
106
+ next unless page.key?(dim)
107
+
108
+ val = page[dim]
109
+ raise ArgumentError, "page[:#{dim}] must be a float >= 0.0352" unless val.is_a?(Numeric) && val >= 0.0352
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+ require "json"
6
+ require_relative "client"
7
+ require_relative "browser"
8
+ require_relative "user_context"
9
+
10
+ module Bidi2pdf
11
+ module Bidi
12
+ class Session
13
+ SUBSCRIBE_EVENTS = [
14
+ "browsingContext",
15
+ "network",
16
+ "log",
17
+ "script",
18
+ "goog:cdp.Debugger.scriptParsed",
19
+ "goog:cdp.CSS.styleSheetAdded",
20
+ "goog:cdp.Runtime.executionContextsCleared",
21
+ # Tracing
22
+ "goog:cdp.Tracing.tracingComplete",
23
+ "goog:cdp.Network.requestWillBeSent",
24
+ "goog:cdp.Debugger.scriptParsed",
25
+ "goog:cdp.Page.screencastFrame"
26
+ ].freeze
27
+
28
+ attr_reader :port, :websocket_url
29
+
30
+ def initialize(port, headless: true)
31
+ @port = port
32
+ @headless = headless
33
+ @client = nil
34
+ @browser = nil
35
+ @websocket_url = nil
36
+ end
37
+
38
+ def start
39
+ client
40
+ end
41
+
42
+ def client
43
+ @client ||= create_client
44
+ end
45
+
46
+ def close
47
+ client&.send_cmd_and_wait("session.end", {}) do |response|
48
+ Bidi2pdf.logger.debug "Session ended: #{response}"
49
+ @client = nil
50
+ @websocket_url = nil
51
+ @browser = nil
52
+ end
53
+ end
54
+
55
+ def browser
56
+ @browser ||= create_browser
57
+ end
58
+
59
+ def user_contexts
60
+ client&.send_cmd_and_wait("browser.getUserContexts", {}) do |response|
61
+ Bidi2pdf.logger.debug "User contexts: #{response}"
62
+ end
63
+ end
64
+
65
+ def status
66
+ client&.send_cmd_and_wait("session.status", {}) do |response|
67
+ Bidi2pdf.logger.info "Session status: #{response.inspect}"
68
+ end
69
+ end
70
+
71
+ private
72
+
73
+ def create_browser
74
+ start
75
+
76
+ @client.start
77
+ @client.wait_until_open
78
+
79
+ Bidi2pdf.logger.info "Subscribing to events"
80
+
81
+ event_client = Bidi::Client.new(websocket_url).tap(&:start)
82
+ event_client.wait_until_open
83
+
84
+ event_client.on_event(*SUBSCRIBE_EVENTS) do |data|
85
+ Bidi2pdf.logger.debug "Received event: #{data["method"]}, params: #{data["params"]}"
86
+ end
87
+
88
+ Bidi::Browser.new(@client)
89
+ end
90
+
91
+ # rubocop: disable Metrics/AbcSize
92
+ def create_client
93
+ uri = URI("http://localhost:#{port}/session")
94
+ args = %w[
95
+ --disable-gpu
96
+ --disable-popup-blocking
97
+ --disable-hang-monitor
98
+ ]
99
+
100
+ args << "--headless" if @headless
101
+
102
+ session_request = {
103
+ "capabilities" => {
104
+ "alwaysMatch" => {
105
+ "browserName" => "chrome",
106
+ "goog:chromeOptions" => {
107
+ "args" => args
108
+ },
109
+ "goog:prerenderingDisabled" => true,
110
+ "unhandledPromptBehavior" => {
111
+ default: "ignore"
112
+ },
113
+ "acceptInsecureCerts" => true,
114
+ "webSocketUrl" => true
115
+ }
116
+ }
117
+ }
118
+ response = Net::HTTP.post(uri, session_request.to_json, "Content-Type" => "application/json")
119
+ session_data = JSON.parse(response.body)
120
+
121
+ Bidi2pdf.logger.debug "Session data: #{session_data}"
122
+
123
+ session_id = session_data["value"]["sessionId"]
124
+ @websocket_url = session_data["value"]["capabilities"]["webSocketUrl"]
125
+
126
+ Bidi2pdf.logger.info "Created session with ID: #{session_id}"
127
+ Bidi2pdf.logger.info "WebSocket URL: #{@websocket_url}"
128
+
129
+ Bidi::Client.new(@websocket_url).tap(&:start)
130
+ end
131
+
132
+ # rubocop: enable Metrics/AbcSize
133
+ end
134
+ end
135
+ end