4 Commits

6 changed files with 131 additions and 94 deletions

View File

@@ -47,7 +47,9 @@ class W3DHub
W3DHUB_API_ENDPOINT = "https://secure.w3dhub.com".freeze # "https://example.com" # "http://127.0.0.1:9292".freeze # 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 # 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 case backend
when :w3dhub when :w3dhub
endpoint = W3DHUB_API_ENDPOINT endpoint = W3DHUB_API_ENDPOINT
@@ -57,7 +59,15 @@ class W3DHub
endpoint = SERVER_LIST_ENDPOINT endpoint = SERVER_LIST_ENDPOINT
end 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}\"..." } logger.debug(LOG_TAG) { "Fetching #{method.to_s.upcase} \"#{url}\"..." }
@@ -70,7 +80,7 @@ class W3DHub
Sync do Sync do
begin 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) Response.new(status: response.status, body: response.read)
rescue Async::TimeoutError => e rescue Async::TimeoutError => e
@@ -88,17 +98,32 @@ class W3DHub
end end
end end
def self.post(url, headers = DEFAULT_HEADERS, body = nil, backend = :w3dhub) def self.provision_http_client(hostname)
async_http(:post, url, headers, body, backend) # 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 end
def self.get(url, headers = DEFAULT_HEADERS, body = nil, backend = :w3dhub) def self.post(path, headers = DEFAULT_HEADERS, body = nil, backend = :w3dhub)
async_http(:get, url, headers, body, backend) 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 end
# Api.get but handles any URL instead of known hosts # Api.get but handles any URL instead of known hosts
def self.fetch(url, headers = DEFAULT_HEADERS, body = nil, backend = nil) def self.fetch(path, headers = DEFAULT_HEADERS, body = nil, backend = nil)
async_http(:get, url, headers, body, backend) async_http(:get, path, headers, body, backend)
end end
# Method: POST # Method: POST

View File

@@ -746,6 +746,8 @@ class W3DHub
end end
patch_entry = patch_mix.entries.find { |e| e.name.casecmp?(".w3dhub.patch") || e.name.casecmp?(".bhppatch") } patch_entry = patch_mix.entries.find { |e| e.name.casecmp?(".w3dhub.patch") || e.name.casecmp?(".bhppatch") }
patch_entry.read patch_entry.read
# "remove" patch meta file from patch before copying patch data
patch_mix.entries.delete(patch_entry)
patch_info = JSON.parse(patch_entry.blob, symbolize_names: true) patch_info = JSON.parse(patch_entry.blob, symbolize_names: true)
@@ -765,20 +767,15 @@ class W3DHub
patch_info[:updatedFiles].each do |file| patch_info[:updatedFiles].each do |file|
logger.debug(LOG_TAG) { " #{file}" } logger.debug(LOG_TAG) { " #{file}" }
patch = patch_mix.entries.find { |e| e.name.casecmp?(file) } patch_mix.entries.each do |entry|
target = target_mix.entries.find { |e| e.name.casecmp?(file) } target_mix.add_entry(entry: entry, replace: true)
if target
target_mix.entries[target_mix.entries.index(target)] = patch
else
target_mix.entries << patch
end end
end end
logger.info(LOG_TAG) { " Writing updated #{file_path}..." } if patch_info[:updatedFiles].size.positive? logger.info(LOG_TAG) { " Writing updated #{file_path}..." } if patch_info[:updatedFiles].size.positive?
temp_mix_path = "#{temp_path}/#{File.basename(file_path)}" temp_mix_path = "#{temp_path}/#{File.basename(file_path)}"
temp_mix = W3DHub::WWMix.new(path: temp_mix_path) temp_mix = W3DHub::WWMix.new(path: temp_mix_path, encrypted: target_mix.encrypted?)
target_mix.entries.each { |e| temp_mix.add_entry(entry: e) } target_mix.entries.each { |e| temp_mix.add_entry(entry: e, replace: true) }
unless temp_mix.save unless temp_mix.save
raise temp_mix.error_reason raise temp_mix.error_reason
end end

View File

@@ -75,24 +75,24 @@ class W3DHub
result = false result = false
Sync do 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 = W3DHub::Api.provision_http_client(uri.origin).send((package.download_url ? :get : :post), uri.request_uri, headers, body)
response = r if response.success?
if r.success? total_bytes = package.size
total_bytes = package.size
r.each do |chunk| response.each do |chunk|
file.write(chunk) file.write(chunk)
block.call(chunk, total_bytes - file.pos, total_bytes) block.call(chunk, total_bytes - file.pos, total_bytes)
end
result = true
end end
result = true
end end
if response.status == 200 || response.status == 206 binding.irb unless response
if response&.status == 200 || response&.status == 206
result = true result = true
else else
logger.debug(LOG_TAG) { " Failed to retrieve package: (#{package.category}:#{package.subcategory}:#{package.name}:#{package.version})" } 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}" } logger.debug(LOG_TAG) { " Download URL: #{endpoint_download_url}, response: #{response&.status || -1}" }
result = false result = false
ensure
file&.close
response&.close
end end
result result
ensure
file&.close
end end
# Download a W3D Hub package # Download a W3D Hub package

View File

@@ -1,4 +1,4 @@
class W3DHub class W3DHub
DIR_NAME = "W3DHubAlt".freeze DIR_NAME = "W3DHubAlt".freeze
VERSION = "0.9.0".freeze VERSION = "0.9.1".freeze
end end

View File

@@ -1,68 +1,75 @@
class W3DHub class W3DHub
class WebSocketClient class WebSocketClient
def initialize def initialize
@errored = nil @errored = nil
@connection = nil @connection = nil
@events = { @events = {
open: nil, open: nil,
message: nil, message: nil,
close: nil, close: nil,
error: nil error: nil
} }
end end
def connect(endpoint, headers: nil, &block) def connect(endpoint, headers: nil, &block)
yield(self) yield(self)
Sync do |task| Sync do |task|
endpoint = Async::HTTP::Endpoint.parse(endpoint, alpn_protocols: Async::HTTP::Protocol::HTTP11.names) 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| endpoint = Async::HTTP::Endpoint.parse(endpoint, alpn_protocols: Async::HTTP::Protocol::HTTP11.names, ssl_context: ssl_context)
@connection = connection
@events[:open]&.call Async::WebSocket::Client.connect(endpoint, headers: headers) do |connection|
@connection = connection
while message = connection.read @events[:open]&.call
@events[:message].call(message)
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 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
end
self self
end end
def on(event, &block) def on(event, &block)
raise "Event must be a symbol" unless event.is_a?(Symbol) raise "Event must be a symbol" unless event.is_a?(Symbol)
raise "Unknown event: #{event.inspect}" unless @events.keys.include?(event) raise "Unknown event: #{event.inspect}" unless @events.keys.include?(event)
raise "No block given for #{event.inspect}" unless block_given? raise "No block given for #{event.inspect}" unless block_given?
@events[event] = block @events[event] = block
end end
def send(data, type: :text) def send(data, type: :text)
@connection&.write(data) @connection&.write(data)
@connection&.flush @connection&.flush
end end
def close def close
@connection&.close @connection&.close
end end
def open? def open?
!closed? !closed?
end end
def closed? def closed?
@connection&.closed? @connection&.closed?
end end
end end
end end

View File

@@ -228,27 +228,34 @@ class W3DHub
@encrypted @encrypted
end end
def add_file(path:) def add_file(path:, replace: false)
return false unless File.exist?(path) return false unless File.exist?(path)
return false if File.directory?(path) return false if File.directory?(path)
info = EntryInfoHeader.new(0, 0, File.size(path)) entry = Entry.new(name: File.basename(path), path: path, info: EntryInfoHeader.new(0, 0, File.size(path)))
@entries << Entry.new(name: File.basename(path), path: path, info: info) add_entry(entry: entry, replace: replace)
true
end end
def add_blob(path:, blob:) def add_blob(path:, blob:, replace: false)
info = EntryInfoHeader.new(0, 0, blob.size) info = EntryInfoHeader.new(0, 0, blob.size)
@entries << Entry.new(name: File.basename(path), path: path, info: info, blob: blob) entry = Entry.new(name: File.basename(path), path: path, info: info, blob: blob)
into.crc32 = @entries.last.calculate_crc32 into.crc32 = @entries.last.calculate_crc32
true add_entry(entry: entry, replace: replace)
end end
def add_entry(entry:) def add_entry(entry:, replace: false)
@entries << entry duplicate = @entries.find { |e| e.name.upcase == entry.name.upcase }
if duplicate
if replace
@entries.delete(duplicate)
else
return false
end
end
@entries << entry
true true
end end