diff --git a/lib/api.rb b/lib/api.rb index 4f44e8b..425c3dc 100644 --- a/lib/api.rb +++ b/lib/api.rb @@ -47,7 +47,9 @@ class W3DHub W3DHUB_API_ENDPOINT = "https://secure.w3dhub.com".freeze # "https://example.com" # "http://127.0.0.1:9292".freeze # ALT_W3DHUB_API_ENDPOINT = "https://w3dhub-api.w3d.cyberarm.dev".freeze # "https://secure.w3dhub.com".freeze # "https://example.com" # "http://127.0.0.1:9292".freeze # - def self.async_http(method, url, headers = DEFAULT_HEADERS, body = nil, backend = :w3dhub) + HTTP_CLIENTS = {} + + def self.async_http(method, path, headers = DEFAULT_HEADERS, body = nil, backend = :w3dhub) case backend when :w3dhub endpoint = W3DHUB_API_ENDPOINT @@ -57,7 +59,15 @@ class W3DHub endpoint = SERVER_LIST_ENDPOINT end - url = "#{endpoint}#{url}" unless url.start_with?("http") + # Handle arbitrary urls that may come through + url = nil + if path.start_with?("http") + uri = URI(path) + endpoint = uri.origin + path = uri.request_uri + else + url = "#{endpoint}#{path}" + end logger.debug(LOG_TAG) { "Fetching #{method.to_s.upcase} \"#{url}\"..." } @@ -70,7 +80,7 @@ class W3DHub Sync do begin - response = Async::HTTP::Internet.send(method, url, headers, body) + response = provision_http_client(endpoint).send(method, path, headers, body) Response.new(status: response.status, body: response.read) rescue Async::TimeoutError => e @@ -88,17 +98,32 @@ class W3DHub end end - def self.post(url, headers = DEFAULT_HEADERS, body = nil, backend = :w3dhub) - async_http(:post, url, headers, body, backend) + def self.provision_http_client(hostname) + # Pin http clients to their host Thread so the fiber scheduler doesn't get upset and raise an error + HTTP_CLIENTS[Thread.current] ||= {} + return HTTP_CLIENTS[Thread.current][hostname.downcase] if HTTP_CLIENTS[Thread.current][hostname.downcase] + + ssl_context = W3DHub.ca_bundle_path ? OpenSSL::SSL::SSLContext.new : nil + ssl_context&.set_params( + ca_file: W3DHub.ca_bundle_path, + verify_mode: OpenSSL::SSL::VERIFY_PEER + ) + + endpoint = Async::HTTP::Endpoint.parse(hostname, ssl_context: ssl_context) + HTTP_CLIENTS[Thread.current][hostname.downcase] = Async::HTTP::Client.new(endpoint) end - def self.get(url, headers = DEFAULT_HEADERS, body = nil, backend = :w3dhub) - async_http(:get, url, headers, body, backend) + def self.post(path, headers = DEFAULT_HEADERS, body = nil, backend = :w3dhub) + async_http(:post, path, headers, body, backend) + end + + def self.get(path, headers = DEFAULT_HEADERS, body = nil, backend = :w3dhub) + async_http(:get, path, headers, body, backend) end # Api.get but handles any URL instead of known hosts - def self.fetch(url, headers = DEFAULT_HEADERS, body = nil, backend = nil) - async_http(:get, url, headers, body, backend) + def self.fetch(path, headers = DEFAULT_HEADERS, body = nil, backend = nil) + async_http(:get, path, headers, body, backend) end # Method: POST diff --git a/lib/cache.rb b/lib/cache.rb index 003d822..54b1988 100644 --- a/lib/cache.rb +++ b/lib/cache.rb @@ -75,24 +75,24 @@ class W3DHub result = false Sync do - response = nil + uri = URI(endpoint_download_url) - Async::HTTP::Internet.send(package.download_url ? :get : :post, endpoint_download_url, headers, body) do |r| - response = r - if r.success? - total_bytes = package.size + response = W3DHub::Api.provision_http_client(uri.origin).send((package.download_url ? :get : :post), uri.request_uri, headers, body) + if response.success? + total_bytes = package.size - r.each do |chunk| - file.write(chunk) + response.each do |chunk| + file.write(chunk) - block.call(chunk, total_bytes - file.pos, total_bytes) - end - - result = true + block.call(chunk, total_bytes - file.pos, total_bytes) end + + result = true end - if response.status == 200 || response.status == 206 + binding.irb unless response + + if response&.status == 200 || response&.status == 206 result = true else logger.debug(LOG_TAG) { " Failed to retrieve package: (#{package.category}:#{package.subcategory}:#{package.name}:#{package.version})" } @@ -114,11 +114,12 @@ class W3DHub logger.debug(LOG_TAG) { " Download URL: #{endpoint_download_url}, response: #{response&.status || -1}" } result = false + ensure + file&.close + response&.close end result - ensure - file&.close end # Download a W3D Hub package diff --git a/lib/websocket_client.rb b/lib/websocket_client.rb index 10f668f..3292a45 100644 --- a/lib/websocket_client.rb +++ b/lib/websocket_client.rb @@ -1,68 +1,75 @@ class W3DHub - class WebSocketClient - def initialize - @errored = nil - @connection = nil + class WebSocketClient + def initialize + @errored = nil + @connection = nil - @events = { - open: nil, - message: nil, - close: nil, - error: nil - } - end + @events = { + open: nil, + message: nil, + close: nil, + error: nil + } + end - def connect(endpoint, headers: nil, &block) - yield(self) + def connect(endpoint, headers: nil, &block) + yield(self) - Sync do |task| - endpoint = Async::HTTP::Endpoint.parse(endpoint, alpn_protocols: Async::HTTP::Protocol::HTTP11.names) + Sync do |task| + ssl_context = W3DHub.ca_bundle_path ? OpenSSL::SSL::SSLContext.new : nil + ssl_context&.alpn_protocols = Async::HTTP::Protocol::HTTP11.names + ssl_context&.set_params( + ca_file: W3DHub.ca_bundle_path, + verify_mode: OpenSSL::SSL::VERIFY_PEER + ) - Async::WebSocket::Client.connect(endpoint, headers: headers) do |connection| - @connection = connection + endpoint = Async::HTTP::Endpoint.parse(endpoint, alpn_protocols: Async::HTTP::Protocol::HTTP11.names, ssl_context: ssl_context) - @events[:open]&.call + Async::WebSocket::Client.connect(endpoint, headers: headers) do |connection| + @connection = connection - while message = connection.read - @events[:message].call(message) + @events[:open]&.call + + while message = connection.read + @events[:message].call(message) + end + # FIXME: Don't rescue for all ta errors? + rescue => error + @errored = true + @events[:error]&.call(error) + ensure + @events[:close]&.call unless @errored + @connection = nil + @errored = false end - # FIXME: Don't rescue for all ta errors? - rescue => error - @errored = true - @events[:error]&.call(error) - ensure - @events[:close]&.call unless @errored - @connection = nil - @errored = false end - end - self - end + self + end - def on(event, &block) - raise "Event must be a symbol" unless event.is_a?(Symbol) - raise "Unknown event: #{event.inspect}" unless @events.keys.include?(event) - raise "No block given for #{event.inspect}" unless block_given? + def on(event, &block) + raise "Event must be a symbol" unless event.is_a?(Symbol) + raise "Unknown event: #{event.inspect}" unless @events.keys.include?(event) + raise "No block given for #{event.inspect}" unless block_given? - @events[event] = block - end + @events[event] = block + end - def send(data, type: :text) - @connection&.write(data) - @connection&.flush - end + def send(data, type: :text) + @connection&.write(data) + @connection&.flush + end - def close - @connection&.close - end + def close + @connection&.close + end - def open? - !closed? - end + def open? + !closed? + end - def closed? - @connection&.closed? - end - end + def closed? + @connection&.closed? + end + end end