22local url = ...
33local socket = require (" socket" )
44local server = assert (socket .bind (" *" , 49082 ) or socket .bind (" *" , 49083 ) or socket .bind (" *" , 49084 ))
5- server :settimeout (30 )
65local host , port = server :getsockname ()
76ConPrintf (" 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)
1211ConPrintf (" Redirect URI: %s" , redirect_uri )
@@ -95,18 +94,35 @@ local commonResponseEnd = [[
9594ConPrintf (" Opening URL: %s" , url )
9695OpenURL (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
137198end
199+ server :close ()
138200return code , state , port
0 commit comments