Replace cookie HTTP verification with butlerd's Profile.List + UseSavedLogin
Some checks failed
Build plugin / build (push) Has been cancelled
Some checks failed
Build plugin / build (push) Has been cancelled
Butlerd stores credentials in its SQLite DB after the first password login. On restart, Profile.List finds saved profiles and Profile.UseSavedLogin refreshes them — no cookie persistence needed on our side.
This commit is contained in:
parent
1931ae143a
commit
d09bd2ae79
3 changed files with 37 additions and 114 deletions
|
|
@ -56,7 +56,6 @@ signal app_installed(cave_id: String, success: bool)
|
|||
signal app_updated(cave_id: String, success: bool)
|
||||
signal app_uninstalled(cave_id: String, success: bool)
|
||||
signal launch_exited(cave_id: String)
|
||||
signal login_cookies_saved(username: String, cookie: String)
|
||||
|
||||
var proc: InteractiveProcess
|
||||
var socket: StreamPeerTCP
|
||||
|
|
@ -385,109 +384,41 @@ func _login_with_password(username: String, password: String) -> void:
|
|||
return
|
||||
profile = res.get("profile", {})
|
||||
is_logged_in = true
|
||||
# Persist the cookie so we can re-authenticate on restart. Butlerd
|
||||
# stores tokens in its own DB, but we re-send the cookie on subsequent
|
||||
# startups so butlerd can refresh the session if needed.
|
||||
var cookie_dict: Dictionary = res.get("cookie", {})
|
||||
if not cookie_dict.is_empty():
|
||||
var cookie_json := JSON.stringify(cookie_dict)
|
||||
emit_signal.call_deferred("login_cookies_saved", username, cookie_json)
|
||||
emit_signal.call_deferred("logged_in", LOGIN_STATUS.OK, profile)
|
||||
|
||||
|
||||
## Re-authenticates using a previously saved cookie. The cookie is a JSON
|
||||
## dict of name-value pairs from Profile.LoginWithPassword. We send it to
|
||||
## itch.io's API directly to verify the session — butlerd doesn't expose a
|
||||
## "login with cookie" method, but it stores the credentials internally so
|
||||
## subsequent butlerd calls will work once we've verified the session.
|
||||
func login_with_cookie(cookie_json: String) -> void:
|
||||
await thread_group.exec(_login_with_cookie.bind(cookie_json))
|
||||
## Attempts to resume a previous login using butlerd's stored credentials.
|
||||
## Returns true on success (profile is set, logged_in signal emitted).
|
||||
func try_saved_login() -> bool:
|
||||
return await thread_group.exec(_try_saved_login)
|
||||
|
||||
|
||||
func _login_with_cookie(cookie_json: String) -> void:
|
||||
var cookie_dict: Dictionary = {}
|
||||
var json := JSON.new()
|
||||
if json.parse(cookie_json) == OK and typeof(json.data) == TYPE_DICTIONARY:
|
||||
cookie_dict = json.data
|
||||
if cookie_dict.is_empty():
|
||||
is_logged_in = false
|
||||
emit_signal.call_deferred("logged_in", LOGIN_STATUS.INVALID_KEY, {})
|
||||
return
|
||||
|
||||
# Build Cookie header from the dict: "name1=value1; name2=value2"
|
||||
var pairs: PackedStringArray = []
|
||||
for key in cookie_dict:
|
||||
pairs.append(str(key) + "=" + str(cookie_dict[key]))
|
||||
var cookie_header := "; ".join(pairs)
|
||||
|
||||
var http := HTTPClient.new()
|
||||
var err := http.connect_to_host("api.itch.io", 443, TLSOptions.client())
|
||||
if err != OK:
|
||||
is_logged_in = false
|
||||
emit_signal.call_deferred("logged_in", LOGIN_STATUS.INVALID_KEY, {})
|
||||
return
|
||||
var deadline := Time.get_ticks_msec() + 15000
|
||||
while http.get_status() == HTTPClient.STATUS_CONNECTING or http.get_status() == HTTPClient.STATUS_RESOLVING:
|
||||
http.poll()
|
||||
if Time.get_ticks_msec() > deadline:
|
||||
http.close()
|
||||
is_logged_in = false
|
||||
emit_signal.call_deferred("logged_in", LOGIN_STATUS.INVALID_KEY, {})
|
||||
return
|
||||
OS.delay_msec(50)
|
||||
if http.get_status() != HTTPClient.STATUS_CONNECTED:
|
||||
http.close()
|
||||
is_logged_in = false
|
||||
emit_signal.call_deferred("logged_in", LOGIN_STATUS.INVALID_KEY, {})
|
||||
return
|
||||
|
||||
var headers := PackedStringArray([
|
||||
"Cookie: " + cookie_header,
|
||||
"User-Agent: opencode-itch-plugin/1.0",
|
||||
])
|
||||
http.request(HTTPClient.METHOD_GET, "/profile", headers)
|
||||
deadline = Time.get_ticks_msec() + 15000
|
||||
while http.get_status() == HTTPClient.STATUS_REQUESTING:
|
||||
http.poll()
|
||||
if Time.get_ticks_msec() > deadline:
|
||||
http.close()
|
||||
is_logged_in = false
|
||||
emit_signal.call_deferred("logged_in", LOGIN_STATUS.INVALID_KEY, {})
|
||||
return
|
||||
OS.delay_msec(10)
|
||||
|
||||
var body := PackedByteArray()
|
||||
while http.get_status() == HTTPClient.STATUS_BODY:
|
||||
http.poll()
|
||||
var chunk: PackedByteArray = http.read_response_body_chunk()
|
||||
if chunk.is_empty():
|
||||
if Time.get_ticks_msec() > deadline:
|
||||
http.close()
|
||||
is_logged_in = false
|
||||
emit_signal.call_deferred("logged_in", LOGIN_STATUS.INVALID_KEY, {})
|
||||
return
|
||||
OS.delay_msec(10)
|
||||
continue
|
||||
body.append_array(chunk)
|
||||
http.close()
|
||||
|
||||
var response_code := http.get_response_code()
|
||||
var response_text := body.get_string_from_utf8()
|
||||
if response_code != 200 or response_text.is_empty():
|
||||
is_logged_in = false
|
||||
emit_signal.call_deferred("logged_in", LOGIN_STATUS.INVALID_KEY, {})
|
||||
return
|
||||
|
||||
var resp_json := JSON.new()
|
||||
if resp_json.parse(response_text) != OK:
|
||||
is_logged_in = false
|
||||
emit_signal.call_deferred("logged_in", LOGIN_STATUS.INVALID_KEY, {})
|
||||
return
|
||||
|
||||
var data: Dictionary = resp_json.data
|
||||
profile = data.get("user", data)
|
||||
func _try_saved_login() -> bool:
|
||||
var list_res := await _rpc_call("Profile.List", {})
|
||||
if "error" in list_res:
|
||||
return false
|
||||
var profiles: Array = list_res.get("profiles", [])
|
||||
if profiles.is_empty():
|
||||
return false
|
||||
# Use the most recently connected profile.
|
||||
var best: Dictionary = profiles[0]
|
||||
var best_time: int = int(best.get("lastConnected", 0))
|
||||
for p in profiles:
|
||||
var t: int = int(p.get("lastConnected", 0))
|
||||
if t > best_time:
|
||||
best = p
|
||||
best_time = t
|
||||
var profile_id: int = int(best.get("id", 0))
|
||||
if profile_id == 0:
|
||||
return false
|
||||
var use_res := await _rpc_call("Profile.UseSavedLogin", {"profileId": profile_id})
|
||||
if "error" in use_res:
|
||||
logger.warn("Profile.UseSavedLogin failed: " + str(use_res["error"]))
|
||||
return false
|
||||
profile = use_res.get("profile", {})
|
||||
is_logged_in = true
|
||||
emit_signal.call_deferred("logged_in", LOGIN_STATUS.OK, profile)
|
||||
return true
|
||||
|
||||
|
||||
## Returns every game the logged-in profile owns a download key for.
|
||||
|
|
|
|||
|
|
@ -100,7 +100,6 @@ func _on_save_button() -> void:
|
|||
|
||||
if method == 0:
|
||||
# API key login
|
||||
settings_manager.set_value("plugin.itch", "cookie", "")
|
||||
settings_manager.set_value("plugin.itch", "username", "")
|
||||
var api_key: String = api_key_box.text.strip_edges()
|
||||
settings_manager.set_value("plugin.itch", "api_key", api_key)
|
||||
|
|
@ -112,7 +111,6 @@ func _on_save_button() -> void:
|
|||
var uname: String = username_box.text.strip_edges()
|
||||
var password: String = password_box.text
|
||||
settings_manager.set_value("plugin.itch", "username", uname)
|
||||
settings_manager.set_value("plugin.itch", "cookie", "")
|
||||
if uname == "" or password == "":
|
||||
return
|
||||
itch.login_with_password(uname, password)
|
||||
|
|
|
|||
24
plugin.gd
24
plugin.gd
|
|
@ -18,7 +18,6 @@ var itch: ItchClient
|
|||
var api_key := settings_manager.get_value("plugin.itch", "api_key", "") as String
|
||||
var login_method := settings_manager.get_value("plugin.itch", "login_method", 0) as int
|
||||
var username := settings_manager.get_value("plugin.itch", "username", "") as String
|
||||
var saved_cookie := settings_manager.get_value("plugin.itch", "cookie", "") as String
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
|
|
@ -30,7 +29,6 @@ func _ready() -> void:
|
|||
itch.bootstrap_finished.connect(_on_client_start)
|
||||
itch.client_ready.connect(_on_client_ready)
|
||||
itch.logged_in.connect(_on_client_logged_in)
|
||||
itch.login_cookies_saved.connect(_on_cookies_saved)
|
||||
add_child(itch)
|
||||
|
||||
# Load the Library implementation
|
||||
|
|
@ -50,6 +48,10 @@ func _on_client_start() -> void:
|
|||
|
||||
# Triggers when butlerd has completed its handshake and is ready for calls
|
||||
func _on_client_ready() -> void:
|
||||
# Always try butlerd's stored credentials first — this handles password
|
||||
# logins from previous runs without any stored tokens on our side.
|
||||
if await itch.try_saved_login():
|
||||
return
|
||||
if login_method == 0:
|
||||
# API key login
|
||||
if api_key == "":
|
||||
|
|
@ -60,20 +62,17 @@ func _on_client_ready() -> void:
|
|||
return
|
||||
itch.login_with_api_key(api_key)
|
||||
else:
|
||||
# Username/password login — try saved cookie first.
|
||||
# Username/password login
|
||||
if username == "":
|
||||
var notify := Notification.new("itch.io username required")
|
||||
notify.icon = icon
|
||||
logger.info(notify.text)
|
||||
notification_manager.show(notify)
|
||||
return
|
||||
if saved_cookie != "":
|
||||
itch.login_with_cookie(saved_cookie)
|
||||
else:
|
||||
var notify := Notification.new("itch.io: open plugin settings to log in")
|
||||
notify.icon = icon
|
||||
logger.info(notify.text)
|
||||
notification_manager.show(notify)
|
||||
var notify := Notification.new("itch.io: open plugin settings to log in")
|
||||
notify.icon = icon
|
||||
logger.info(notify.text)
|
||||
notification_manager.show(notify)
|
||||
|
||||
|
||||
# Triggers when the itch client finishes a login attempt
|
||||
|
|
@ -93,11 +92,6 @@ func _on_client_logged_in(status: ItchClient.LOGIN_STATUS, profile: Dictionary)
|
|||
notification_manager.show(notify)
|
||||
|
||||
|
||||
func _on_cookies_saved(uname: String, cookie: String) -> void:
|
||||
settings_manager.set_value("plugin.itch", "username", uname)
|
||||
settings_manager.set_value("plugin.itch", "cookie", cookie)
|
||||
|
||||
|
||||
# Return the settings menu scene
|
||||
func get_settings_menu() -> Control:
|
||||
return settings_menu.instantiate()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue