2 Commits

8 changed files with 251 additions and 220 deletions

View File

@@ -43,7 +43,7 @@ GEM
fiber-storage (1.0.1) fiber-storage (1.0.1)
fiddle (1.1.8) fiddle (1.1.8)
gosu (1.4.6) gosu (1.4.6)
io-endpoint (0.17.1) io-endpoint (0.17.2)
io-event (1.14.2) io-event (1.14.2)
io-stream (0.11.1) io-stream (0.11.1)
ircparser (1.0.0) ircparser (1.0.0)
@@ -97,4 +97,4 @@ DEPENDENCIES
win32-security win32-security
BUNDLED WITH BUNDLED WITH
2.6.8 4.0.3

View File

@@ -16,32 +16,7 @@ class W3DHub
].freeze ].freeze
def self.on_thread(method, *args, &callback) def self.on_thread(method, *args, &callback)
raise "Renew." Api.send(method, *args, &callback)
BackgroundWorker.foreground_job(-> { Api.send(method, *args) }, callback)
end
class Response
def initialize(error: nil, status: -1, body: "")
@status = status
@body = body
@error = error
end
def success?
@status == 200
end
def status
@status
end
def body
@body
end
def error
@error
end
end end
#! === W3D Hub API === !# #! === W3D Hub API === !#
@@ -50,7 +25,9 @@ class W3DHub
HTTP_CLIENTS = {} HTTP_CLIENTS = {}
def self.async_http(method, path, headers = DEFAULT_HEADERS, body = nil, backend = :w3dhub) def self.async_http(method, path, headers = DEFAULT_HEADERS, body = nil, backend = :w3dhub, &callback)
raise "NO CALLBACK DEFINED!" unless callback
case backend case backend
when :w3dhub when :w3dhub
endpoint = W3DHUB_API_ENDPOINT endpoint = W3DHUB_API_ENDPOINT
@@ -79,24 +56,7 @@ class W3DHub
headers << ["authorization", "Bearer #{Store.account.access_token}"] headers << ["authorization", "Bearer #{Store.account.access_token}"]
end end
Sync do Store.network_manager.request(method, url, headers, body, nil, &callback)
begin
response = provision_http_client(endpoint).send(method, path, headers, body)
Response.new(status: response.status, body: response.read)
rescue Async::TimeoutError => e
logger.error(LOG_TAG) { "Connection to \"#{url}\" timed out after: #{API_TIMEOUT} seconds" }
Response.new(error: e)
rescue StandardError => e
logger.error(LOG_TAG) { "Connection to \"#{url}\" errored:" }
logger.error(LOG_TAG) { e }
Response.new(error: e)
ensure
response&.close
end
end
end end
def self.provision_http_client(hostname) def self.provision_http_client(hostname)
@@ -114,17 +74,17 @@ class W3DHub
HTTP_CLIENTS[Thread.current][hostname.downcase] = Async::HTTP::Client.new(endpoint) HTTP_CLIENTS[Thread.current][hostname.downcase] = Async::HTTP::Client.new(endpoint)
end end
def self.post(path, headers = DEFAULT_HEADERS, body = nil, backend = :w3dhub) def self.post(path, headers = DEFAULT_HEADERS, body = nil, backend = :w3dhub, &callback)
async_http(:post, path, headers, body, backend) async_http(:post, path, headers, body, backend, &callback)
end end
def self.get(path, headers = DEFAULT_HEADERS, body = nil, backend = :w3dhub) def self.get(path, headers = DEFAULT_HEADERS, body = nil, backend = :w3dhub, &callback)
async_http(:get, path, headers, body, backend) async_http(:get, path, headers, body, backend, &callback)
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(path, headers = DEFAULT_HEADERS, body = nil, backend = nil) def self.fetch(path, headers = DEFAULT_HEADERS, body = nil, backend = nil, &callback)
async_http(:get, path, headers, body, backend) async_http(:get, path, headers, body, backend, &callback)
end end
# Method: POST # Method: POST
@@ -142,92 +102,113 @@ class W3DHub
# #
# On a failed login the service responds with: # On a failed login the service responds with:
# {"error":"login-failed"} # {"error":"login-failed"}
def self.refresh_user_login(refresh_token, backend = :w3dhub) def self.refresh_user_login(refresh_token, backend = :w3dhub, &callback)
body = URI.encode_www_form("data": JSON.dump({refreshToken: refresh_token})) handler = lambda do |result|
response = post("/apis/launcher/1/user-login", FORM_ENCODED_HEADERS, body, backend) if result.okay?
user_data = JSON.parse(result.data, symbolize_names: true)
if response.status == 200 if user_data[:error]
user_data = JSON.parse(response.body, symbolize_names: true) callback.call(false)
next
return false if user_data[:error] end
user_details_data = user_details(user_data[:userid]) || {} user_details_data = user_details(user_data[:userid]) || {}
Account.new(user_data, user_details_data) callback.call(Account.new(user_data, user_details_data))
else else
logger.error(LOG_TAG) { "Failed to fetch refresh user login:" } logger.error(LOG_TAG) { "Failed to fetch refresh user login:" }
logger.error(LOG_TAG) { response } logger.error(LOG_TAG) { result.error }
false
callback.call(false)
end end
end end
body = URI.encode_www_form("data": JSON.dump({ refreshToken: refresh_token }))
post("/apis/launcher/1/user-login", FORM_ENCODED_HEADERS, body, backend, &handler)
end
# See #user_refresh_token # See #user_refresh_token
def self.user_login(username, password, backend = :w3dhub) def self.user_login(username, password, backend = :w3dhub, &callback)
body = URI.encode_www_form("data": JSON.dump({username: username, password: password})) handler = lambda do |result|
response = post("/apis/launcher/1/user-login", FORM_ENCODED_HEADERS, body, backend) if result.okay?
user_data = JSON.parse(result.data, symbolize_names: true)
if response.status == 200 if user_data[:error]
user_data = JSON.parse(response.body, symbolize_names: true) callback.call(false)
next
return false if user_data[:error] end
user_details_data = user_details(user_data[:userid]) || {} user_details_data = user_details(user_data[:userid]) || {}
Account.new(user_data, user_details_data) callback.call(Account.new(user_data, user_details_data))
else else
logger.error(LOG_TAG) { "Failed to fetch user login:" } logger.error(LOG_TAG) { "Failed to fetch user login:" }
logger.error(LOG_TAG) { response } logger.error(LOG_TAG) { result.error }
false
callback.call(false)
end end
end end
body = URI.encode_www_form("data": JSON.dump({ username: username, password: password }))
post("/apis/launcher/1/user-login", FORM_ENCODED_HEADERS, body, backend, &handler)
end
# /apis/w3dhub/1/get-user-details # /apis/w3dhub/1/get-user-details
# #
# Response: avatar-uri (Image download uri), id, username # Response: avatar-uri (Image download uri), id, username
def self.user_details(id, backend = :w3dhub) def self.user_details(id, backend = :w3dhub, &callback)
body = URI.encode_www_form("data": JSON.dump({ id: id })) handler = lambda do |result|
user_details = post("/apis/w3dhub/1/get-user-details", FORM_ENCODED_HEADERS, body, backend) if result.okay?
callback.call(JSON.parse(result.data, symbolize_names: true))
if user_details.status == 200
JSON.parse(user_details.body, symbolize_names: true)
else else
logger.error(LOG_TAG) { "Failed to fetch user details:" } logger.error(LOG_TAG) { "Failed to fetch user details:" }
logger.error(LOG_TAG) { user_details } logger.error(LOG_TAG) { result.error }
false
callback.call(false)
end end
end end
body = URI.encode_www_form("data": JSON.dump({ id: id }))
post("/apis/w3dhub/1/get-user-details", FORM_ENCODED_HEADERS, body, backend, &handler)
end
# /apis/w3dhub/1/get-service-status # /apis/w3dhub/1/get-service-status
# Service response: # Service response:
# {"services":{"authentication":true,"packageDownload":true}} # {"services":{"authentication":true,"packageDownload":true}}
def self.service_status(backend = :w3dhub) def self.service_status(backend = :w3dhub, &callback)
response = post("/apis/w3dhub/1/get-service-status", DEFAULT_HEADERS, nil, backend) handler = lambda do |result|
if result.okay?
if response.status == 200 callback.call(ServiceStatus.new(result.data))
ServiceStatus.new(response.body)
else else
logger.error(LOG_TAG) { "Failed to fetch service status:" } logger.error(LOG_TAG) { "Failed to fetch service status:" }
logger.error(LOG_TAG) { response } logger.error(LOG_TAG) { result.error }
false
callback.call(false)
end end
end end
post("/apis/w3dhub/1/get-service-status", DEFAULT_HEADERS, nil, backend, &handler)
end
# /apis/launcher/1/get-applications # /apis/launcher/1/get-applications
# Client sends an Authorization header bearer token which is received from logging in (Optional) # Client sends an Authorization header bearer token which is received from logging in (Optional)
# Launcher sends an empty data request: data={} # Launcher sends an empty data request: data={}
# Response is a list of applications/games # Response is a list of applications/games
def self.applications(backend = :w3dhub) def self.applications(backend = :w3dhub, &callback)
response = post("/apis/launcher/1/get-applications", DEFAULT_HEADERS, nil, backend) handler = lambda do |result|
if result.okay?
if response.status == 200 callback.call(Applications.new(result.data, backend))
Applications.new(response.body, backend)
else else
logger.error(LOG_TAG) { "Failed to fetch applications list:" } logger.error(LOG_TAG) { "Failed to fetch applications list:" }
logger.error(LOG_TAG) { response } logger.error(LOG_TAG) { result.error }
false
callback.call(false)
end end
end end
post("/apis/launcher/1/get-applications", DEFAULT_HEADERS, nil, backend, &handler)
end
# Populate applications list from primary and alternate backends # Populate applications list from primary and alternate backends
# (alternate only has latest public builds of _most_ games) # (alternate only has latest public builds of _most_ games)
def self._applications def self._applications
@@ -301,63 +282,72 @@ class W3DHub
# Client sends an Authorization header bearer token which is received from logging in (Optional) # Client sends an Authorization header bearer token which is received from logging in (Optional)
# Client requests news for a specific application/game e.g.: data={"category":"ia"} ("launcher-home" retrieves the weekly hub updates) # Client requests news for a specific application/game e.g.: data={"category":"ia"} ("launcher-home" retrieves the weekly hub updates)
# Response is a JSON hash with a "highlighted" and "news" keys; the "news" one seems to be the desired one # Response is a JSON hash with a "highlighted" and "news" keys; the "news" one seems to be the desired one
def self.news(category, backend = :w3dhub) def self.news(category, backend = :w3dhub, &callback)
body = URI.encode_www_form("data": JSON.dump({category: category})) handler = lambda do |result|
response = post("/apis/w3dhub/1/get-news", FORM_ENCODED_HEADERS, body, backend) if result.okay?
callback.call(News.new(result.data))
if response.status == 200
News.new(response.body)
else else
logger.error(LOG_TAG) { "Failed to fetch news for:" } logger.error(LOG_TAG) { "Failed to fetch news for:" }
logger.error(LOG_TAG) { category } logger.error(LOG_TAG) { category }
logger.error(LOG_TAG) { response } logger.error(LOG_TAG) { result.error }
false
callback.call(false)
end end
end end
body = URI.encode_www_form("data": JSON.dump({ category: category }))
post("/apis/w3dhub/1/get-news", FORM_ENCODED_HEADERS, body, backend, &handler)
end
# Downloading games # Downloading games
# /apis/launcher/1/get-package-details # /apis/launcher/1/get-package-details
# client requests package details: data={"packages":[{"category":"games","name":"apb.ico","subcategory":"apb","version":""}]} # client requests package details: data={"packages":[{"category":"games","name":"apb.ico","subcategory":"apb","version":""}]}
def self.package_details(packages, backend = :w3dhub) def self.package_details(packages, backend = :w3dhub, &callback)
body = URI.encode_www_form("data": JSON.dump({ packages: packages })) handler = lambda do |result|
response = post("/apis/launcher/1/get-package-details", FORM_ENCODED_HEADERS, body, backend) if result.okay?
hash = JSON.parse(result.data, symbolize_names: true)
if response.status == 200 callback.call(hash[:packages].map { |pkg| Package.new(pkg) })
hash = JSON.parse(response.body, symbolize_names: true)
hash[:packages].map { |pkg| Package.new(pkg) }
else else
logger.error(LOG_TAG) { "Failed to fetch package details for:" } logger.error(LOG_TAG) { "Failed to fetch package details for:" }
logger.error(LOG_TAG) { packages } logger.error(LOG_TAG) { packages }
logger.error(LOG_TAG) { response } logger.error(LOG_TAG) { result.error }
false
callback.call(false)
end end
end end
body = URI.encode_www_form("data": JSON.dump({ packages: packages }))
post("/apis/launcher/1/get-package-details", FORM_ENCODED_HEADERS, body, backend, &handler)
end
# /apis/launcher/1/get-package # /apis/launcher/1/get-package
# client requests package: data={"category":"games","name":"ECW_Asteroids.zip","subcategory":"ecw","version":"1.0.0.0"} # client requests package: data={"category":"games","name":"ECW_Asteroids.zip","subcategory":"ecw","version":"1.0.0.0"}
# #
# server responds with download bytes, probably supports chunked download and resume # server responds with download bytes, probably supports chunked download and resume
def self.package(package, &block) # FIXME: REFACTOR Cache.fetch_package to use HttpClient
Cache.fetch_package(package, block) def self.package(package, &callback)
Cache.fetch_package(package, callback)
end end
# /apis/w3dhub/1/get-events # /apis/w3dhub/1/get-events
# #
# clients requests events: data={"serverPath":"apb"} # clients requests events: data={"serverPath":"apb"}
def self.events(app_id, backend = :w3dhub) def self.events(app_id, backend = :w3dhub, &callback)
body = URI.encode_www_form("data": JSON.dump({ serverPath: app_id })) handler = lambda do |result|
response = post("/apis/w3dhub/1/get-server-events", FORM_ENCODED_HEADERS, body, backend) if result.okay?
if response.status == 200
array = JSON.parse(response.body, symbolize_names: true) array = JSON.parse(response.body, symbolize_names: true)
array.map { |e| Event.new(e) } callback.call(array.map { |e| Event.new(e) })
else else
false callback.call(false)
end end
end end
body = URI.encode_www_form("data": JSON.dump({ serverPath: app_id }))
post("/apis/w3dhub/1/get-server-events", FORM_ENCODED_HEADERS, body, backend, &handler)
end
#! === Server List API === !# #! === Server List API === !#
# SERVER_LIST_ENDPOINT = "https://gsh.w3dhub.com".freeze # SERVER_LIST_ENDPOINT = "https://gsh.w3dhub.com".freeze
@@ -381,15 +371,17 @@ class W3DHub
# id, name, score, kills, deaths # id, name, score, kills, deaths
# ...players[]: # ...players[]:
# nick, team (index of teams array), score, kills, deaths # nick, team (index of teams array), score, kills, deaths
def self.server_list(level = 1, backend = :gsh) def self.server_list(level = 1, backend = :gsh, &callback)
response = get("/listings/getAll/v2?statusLevel=#{level}", DEFAULT_HEADERS, nil, backend) handler = lambda do |result|
if result.okay?
if response.status == 200 data = JSON.parse(result.data, symbolize_names: true)
data = JSON.parse(response.body, symbolize_names: true) callback.call(data.map { |hash| ServerListServer.new(hash) })
return data.map { |hash| ServerListServer.new(hash) } else
callback.call(false)
end
end end
false get("/listings/getAll/v2?statusLevel=#{level}", DEFAULT_HEADERS, nil, backend, &handler)
end end
# /listings/getStatus/v2/:id?statusLevel=#{0-2} # /listings/getStatus/v2/:id?statusLevel=#{0-2}
@@ -403,23 +395,24 @@ class W3DHub
# id, name, score, kills, deaths # id, name, score, kills, deaths
# ...players[]: # ...players[]:
# nick, team (index of teams array), score, kills, deaths # nick, team (index of teams array), score, kills, deaths
def self.server_details(id, level, backend = :gsh) def self.server_details(id, level, backend = :gsh, &callback)
return false unless id && level return false unless id && level
response = get("/listings/getStatus/v2/#{id}?statusLevel=#{level}", DEFAULT_HEADERS, nil, backend) handler = lambda do |result|
if result.okay?
if response.status == 200 callback.call(JSON.parse(response.body, symbolize_names: true))
hash = JSON.parse(response.body, symbolize_names: true) else
return hash callback.call(false)
end
end end
false get("/listings/getStatus/v2/#{id}?statusLevel=#{level}", DEFAULT_HEADERS, nil, backend, &handler)
end end
# /listings/push/v2/negotiate?negotiateVersion=1 # /listings/push/v2/negotiate?negotiateVersion=1
##? /listings/push/v2/?id=#{websocket token?} ##? /listings/push/v2/?id=#{websocket token?}
## Websocket server list listener ## Websocket server list listener
def self.server_list_push(id) def self.server_list_push(id, &callback)
end end
end end
end end

View File

@@ -23,7 +23,7 @@ class W3DHub
@invocation_id = 0 @invocation_id = 0
logger.info(LOG_TAG) { "Starting emulated SignalR Server List Updater..." } logger.info(LOG_TAG) { "Starting emulated SignalR Server List Updater..." }
run # run
end end
def run def run
@@ -32,7 +32,8 @@ class W3DHub
begin begin
@auto_reconnect = true @auto_reconnect = true
while W3DHub::BackgroundWorker.alive? # FIXME
while true #W3DHub::BackgroundWorker.alive?
connect if @auto_reconnect connect if @auto_reconnect
sleep @reconnection_delay sleep @reconnection_delay
end end
@@ -54,19 +55,31 @@ class W3DHub
@auto_reconnect = false @auto_reconnect = false
logger.debug(LOG_TAG) { "Requesting connection token..." } logger.debug(LOG_TAG) { "Requesting connection token..." }
response = Api.post("/listings/push/v2/negotiate?negotiateVersion=1", Api::DEFAULT_HEADERS, "", :gsh)
if response.status != 200 result = nil
Api.post("/listings/push/v2/negotiate?negotiateVersion=1", Api::DEFAULT_HEADERS, "", :gsh) do |callback_result|
result = callback_result
end
# FIXME: we've introduced ourselves to callback hell, yay!
while result.nil?
sleep 0.1
end
if result.error?
@auto_reconnect = true @auto_reconnect = true
@reconnection_delay = @reconnection_delay * 2 @reconnection_delay *= 2
@reconnection_delay = 60 if @reconnection_delay > 60 @reconnection_delay = 60 if @reconnection_delay > 60
return return
end end
@reconnection_delay = 1 @reconnection_delay = 1
data = JSON.parse(response.body, symbolize_names: true) connect_websocket(JSON.parse(result.data, symbolize_names: true))
end
def connect_websocket(data)
@invocation_id = 0 if @invocation_id > 9095 @invocation_id = 0 if @invocation_id > 9095
id = data[:connectionToken] id = data[:connectionToken]
endpoint = "#{Api::SERVER_LIST_ENDPOINT}/listings/push/v2?id=#{id}" endpoint = "#{Api::SERVER_LIST_ENDPOINT}/listings/push/v2?id=#{id}"

View File

@@ -5,6 +5,7 @@ class W3DHub
Request = Struct.new(:context, :callback) Request = Struct.new(:context, :callback)
Context = Data.define( Context = Data.define(
:request_id, :request_id,
:method,
:url, :url,
:headers, :headers,
:body, :body,
@@ -12,52 +13,42 @@ class W3DHub
) )
def initialize def initialize
@requests = {} @requests = []
@running = true
@ractor = Ractor.new do
raise "Something has gone quite wrong!" if Ractor.main?
queue = []
api_client = ApiClient.new
# Ractor has no concept of non-blocking send/receive... :cry:
Thread.new do Thread.new do
while (context = Ractor.receive) # blocking http_client = HttpClient.new
# we cannot (easily) ensure we always are receive expected data
next unless context.is_a?(Context)
queue << context Sync do
end while @running
end request = @requests.shift
Async do # goto sleep for an second if there is no work to be doing
loop do unless request
context = queue.shift sleep 1
# goto sleep for an instant if there is no work to be doing
unless context
sleep 0.1
next next
end end
Sync do Async do |task|
result = api_client.handle(context) assigned_request = request
result = http_client.handle(task, assigned_request)
Ractor.yield(NetworkEvent.new(context, result)) pp [assigned_request, result]
Store.main_thread_queue << -> { assigned_request.callback.call(result) }
end
end end
end end
end end
end end
monitor def request(method, url, headers, body, bearer_token, &block)
end
def add_request(url, headers, body, bearer_token, &block)
request_id = SecureRandom.hex request_id = SecureRandom.hex
@requests << Request.new( request = Request.new(
Context.new( Context.new(
request_id, request_id,
method,
url, url,
headers, headers,
body, body,
@@ -66,31 +57,9 @@ class W3DHub
block block
) )
@ractor.send(context) @requests << request
request_id request_id
end end
def monitor
raise "Something has gone quite wrong!!!" unless Ractor.main?
# Thread that spends its days sleeping **yawn**
Thread.new do
while (event = @ractor.take)
pp event
next unless event.is_a?(NetworkEvent)
request = @request.find { |r| r.context.request_id == event.context.request_id }
next if request
@requests.delete(request)
result = event.result
Store.main_thread_queue << ->(result) { request.callback(result) }
end
end
end
end end
end end

View File

@@ -1,9 +0,0 @@
class W3DHub
class NetworkManager
# Api reimplemented in a Ractor friendly manner
class ApiClient
def initialize
end
end
end
end

View File

@@ -0,0 +1,61 @@
class W3DHub
class NetworkManager
# non-blocking, http requests.
class HttpClient
def initialize
@http_clients = {}
end
def handle(task, request)
result = CyberarmEngine::Result.new
context = request.context
task.with_timeout(30) do
uri = URI(context.url)
pp uri
response = provision_http_client(uri.origin).send(
context.method,
uri.path,
context.headers,
context.body
)
pp response
if response.success?
result.data = response.body.read
else
result.error = response
end
rescue Async::TimeoutError => e
result.error = e
rescue StandardError => e
result.error = e
ensure
response&.close
end
result
end
def provision_http_client(hostname)
return @http_clients[hostname.downcase] if @http_clients[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[hostname.downcase] = Async::HTTP::Client.new(endpoint)
end
def wrapped_error(error)
WrappedError.new(error.class, error.message.to_s, error.backtrace)
end
end
end
end

View File

@@ -175,7 +175,11 @@ class W3DHub
end end
def service_status def service_status
@status_label.value = "Checking service status..." #I18n.t(:"server_browser.fetching_server_list")
Api.on_thread(:service_status) do |service_status| Api.on_thread(:service_status) do |service_status|
pp service_status
@service_status = service_status @service_status = service_status
if @service_status if @service_status
@@ -227,14 +231,14 @@ class W3DHub
def applications def applications
@status_label.value = I18n.t(:"boot.checking_for_updates") @status_label.value = I18n.t(:"boot.checking_for_updates")
Api.on_thread(:_applications) do |applications| # Api.on_thread(:_applications) do |applications|
Api.on_thread(:applications, :alt_w3dhub) do |applications|
if applications if applications
Store.applications = applications Store.applications = applications
Store.settings.save_application_cache(applications.data.to_json) Store.settings.save_application_cache(applications.data.to_json)
@tasks[:applications][:complete] = true @tasks[:applications][:complete] = true
else else
# FIXME: Failed to retreive! @status_label.value = "FAILED TO RETREIVE APPS LIST"
BackgroundWorker.foreground_job(-> {}, ->(_) { @status_label.value = "FAILED TO RETREIVE APPS LIST" })
@offline_mode = true @offline_mode = true
Store.offline_mode = true Store.offline_mode = true

View File

@@ -113,7 +113,7 @@ require_relative "lib/hardware_survey"
require_relative "lib/game_settings" require_relative "lib/game_settings"
require_relative "lib/websocket_client" require_relative "lib/websocket_client"
require_relative "lib/network_manager" require_relative "lib/network_manager"
require_relative "lib/network_manager/api_client" require_relative "lib/network_manager/http_client"
require_relative "lib/application_manager" require_relative "lib/application_manager"
require_relative "lib/application_manager/manifest" require_relative "lib/application_manager/manifest"
require_relative "lib/application_manager/status" require_relative "lib/application_manager/status"