Skip to content

Commit 215977d

Browse files
authored
Fix OAuth2 redirect timeouts caused by closing the server too early (#1285)
* Fix redirect failures caused by closing the server too early * Correct formatting in function doc comment
1 parent 873f311 commit 215977d

1 file changed

Lines changed: 71 additions & 9 deletions

File tree

src/LaunchServer.lua

Lines changed: 71 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@
22
local url = ...
33
local socket = require("socket")
44
local server = assert(socket.bind("*", 49082) or socket.bind("*", 49083) or socket.bind("*", 49084))
5-
server:settimeout(30)
65
local host, port = server:getsockname()
76
ConPrintf("Server started on %s:%s", host, port)
87

9-
local redirect_uri= string.format(
8+
local redirect_uri = string.format(
109
"http://localhost:%d", port
1110
)
1211
ConPrintf("Redirect URI: %s", redirect_uri)
@@ -95,18 +94,35 @@ local commonResponseEnd = [[
9594
ConPrintf("Opening URL: %s", url)
9695
OpenURL(url)
9796

98-
local code, state
99-
local client = server:accept()
100-
if client then
101-
client:settimeout(10)
97+
--- Handle an incoming socket connection, to complete an OAuth redirect.
98+
--- @param client table @The socket connection to handle, as returned by `server:accept()`.
99+
--- @param attempt number @The number of attempts made to handle an incoming connection. This is used for logging
100+
--- purposes, since spurious issues can be difficult to identify otherwise.
101+
--- @return boolean shouldRetry @Whether we should wait for another connection. If false, we've successfully responded
102+
--- to a HTTP request. Note that, for the purposes of this function, we don't care whether authorization was *granted*,
103+
--- just that the process itself was completed and the user was redirected as intended.
104+
--- @return string? code @The OAuth authorization code. This is exchanged for an access token and refresh token later.
105+
--- @return string? state @The OAuth state string. This is a sentinel value used to ensure that a request hasn't been
106+
--- forged.
107+
function handleConnection(client, attempt)
108+
local shouldRetry, code, state = true, nil, nil
109+
102110
local request, err = client:receive("*l")
103-
104-
if not err and request then
111+
if err then
112+
ConPrintf("Attempt %d to handle incoming connection failed: %s", attempt, err)
113+
elseif request then
105114
local response
106115
local _, _, method, path, version = request:find("^(%S+)%s(%S+)%s(%S+)")
107116
if method ~= "GET" then
108-
return
117+
ConPrintf(
118+
"Attempt %d to handle incoming connection received an invalid HTTP request: non-GET method %s",
119+
attempt,
120+
method
121+
)
122+
123+
return true
109124
end
125+
110126
local queryParams = {}
111127
for k, v in path:gmatch("([^&=?]+)=([^&=?]+)") do
112128
queryParams[k] = v:gsub("%%(%x%x)", function(hex)
@@ -129,10 +145,56 @@ if client then
129145
]] .. commonResponseEnd
130146
end
131147

148+
shouldRetry = false
149+
if attempt ~= 1 then
150+
ConPrintf("Attempt %d to handle incoming connection received a valid HTTP request", attempt)
151+
end
152+
132153
-- Send HTTP Response
133154
--ConPrintf("Sending response: %s", response)
134155
client:send(response)
135156
end
157+
158+
return shouldRetry, code, state
159+
end
160+
161+
-- Misbehaving software (think VPNs, anything network-related, even OS services) will occasionally attempt to connect
162+
-- to newly-opened sockets for one reason or another. Previously, PoB only waited for one connection, and gave up
163+
-- immediately if something went wrong.
164+
--
165+
-- This would result in a sequence of events roughly like this:
166+
-- 1. PoB opens a socket
167+
-- 2. A misbehaving piece of software connects to the socket, sends nothing, then terminates the connection
168+
-- 3. PoB tries to read from the socket, receives an error since the connection is terminated, and closes the server
169+
-- 4. OAuth authorization succeeds, but by the time the user is redirected back to PoB, the server is already closed
170+
-- 5. PoB never receives the OAuth redirect, and doesn't have any of the information necessary to use the API
171+
--
172+
-- To avoid this, we instead allow for any number of incoming connections, and simply stop listening for them once
173+
-- either a) 30 seconds have elapsed or b) we've received a legitimate HTTP request and responded to it.
174+
--
175+
-- Unfortunately, this still isn't perfect: in theory, two applications (such as a browser, and something else) could
176+
-- attempt to establish a connection at the same time. In the future, this could be refactored to perform non-blocking
177+
-- IO, so that it can operate concurrently, but hopefully that isn't necessary.
178+
local attempt = 1
179+
local stopAt = os.time() + 30
180+
local shouldRetry, code, state = true, nil, nil
181+
while (os.time() < stopAt) and shouldRetry do
182+
-- `settimeout`` applies only to individual operations, but we're more concerned with not spending more than 30
183+
-- seconds *total* waiting, so we adjust with each iteration as necessary.
184+
local remainingTime = math.max(0, stopAt - os.time())
185+
server:settimeout(remainingTime)
186+
187+
local client = server:accept()
188+
if not client then
189+
goto retry
190+
end
191+
192+
client:settimeout(5)
193+
shouldRetry, code, state = handleConnection(client, attempt)
136194
client:close()
195+
196+
:: retry ::
197+
attempt = attempt + 1
137198
end
199+
server:close()
138200
return code, state, port

0 commit comments

Comments
 (0)