Logout button in popup window works but user gets no feedback
-
Running on an Alix based system currently using pfSense 1.2.3 with captive portal and the logout pop window enabled we notice that when users click on the logout button the session is terminated but the window does not always indicate to the user that the session has ended. The logout status display seems to work fine when using firefox. However, when using safari or chrome (or any derrivative using webkit) after the user pushes the logout button the display remains frozen and the user is never told that the session has logged out. When the user presses the logout button a second or third time the window then displays that the session has ended and the window closes and goes away.
This issue is discussed in an old thread found here
http://forum.pfsense.org/index.php?topic=14322.0
but never resolved.The problem occurs not only with the popup window but with any webpage that uses a POST action to the index.php for logout. This happens, for example, under pfSense 2.0 in /var/etc/captiveportal-logout.html where a post to index.php is made to end the session. As in
LogoutWin.document.write('');
LogoutWin.document.write('');The captive portal code for managing the user popup window in 1.2.3 and 2.0 is basically the same. Once the user is authenticated a popup windows is displayed which contains a "logout" button on a form which when submitted does a POST action to index.php with the session id as an argument. When index.php processes the POST the captive portal session to be terminated.
Looking at the pertinent code in index.php (using pfSense 2.0 code) on line 108 we find the following
if ($_POST['logout_id']) {
disconnect_client($_POST['logout_id']);
echo << <eod<br><title>Disconnecting…</title>You have been disconnected.
EOD;
exit;So the function disconnect_client is called to end the session before the HTML is posted to the browser. Looking a disconnect_client (again pf2.0 code) on line 503 we find
for ($i = 0; $i < $dbcount; $i++) {
if ($cpdb[$i][5] == $sessionid) {
captiveportal_disconnect($cpdb[$i],$radiusservers, $term_cause);
captiveportal_logportalauth($cpdb[$i][4],$cpdb[$i][3],$cpdb[$i][2],$logoutReason);
unset($cpdb[$i]);
break;
}where the session id is found in the captiveportal active session db and is then terminated with captiveportal_disconnect which we find in /etc/inc/captiveportal.php which eventually executes ipfw to shutdown traffic for the specific client.
/* Delete client's ip entry from tables 3 and 4. */
mwexec("/sbin/ipfw table 1 delete {$dbent[2]}");
mwexec("/sbin/ipfw table 2 delete {$dbent[2]}");Since the code that is executing the ipfw deletes happens in the same thread as index.php the socket between the http server and the client is severed and not closed properly. The client never sees the close or receives any indication that the socket has been closed because the IP packets are just dropped…. So... as far as the client is concerned the socket is still open and it remains in a state waiting for data...
So... this causes 2 problems.
1. As stated above disconnect_client is called before the HTML is sent to the client web browser. So a race condition is setup where if the ipfw deletes happen before the HTML code is sent then the browser never sees disconnect text. I suspect that for most systems the HTML writes win over the ipfw delete which causes the text to make it just fine to the browser. So... users using firefox will see the disconnect mesg. This is a race condition which may cause erratic behavior of the logout window.
2. Unlike firefox which will display HTML text as it arrives before the http server closes down the socket, safari and chrome do not. On safari and chrome the HTML status code is received (as viewed in the chrome console) but never displayed. The browser wait indicator just spins and spins and the code never gets rendered.
This causes a problem for users who are not smart enough to refresh the page or in the case of the popup window push the logout button a second time. The status window just "hangs" never displaying the "Disconnecting..." mesg.
The only solution that I can think of to make this work properly is to have index.php spawn a separate process which calls captiveportal_disconnect() after index.php has properly exited. This allows the HTML "Disconnect..." mesg to make it all the way to the browser before the network is disconnected and also allows the web server to properly close down the socket so that the remote web browser can render the page.
Writing an external php script which takes the session_id as an argument and then calls captiveportal_disconnect() should be straight forward. This script could be called with mwexec_bg(). The more difficult thing is how to synchronize the two processes so that the second only executes after the first has exited.
I will play with this further and report back...
Anyone else run into this? I know that there is an old thread that discussed this issue but it seems it was never resolved.
--luis</eod<br>
-
I am not sure to understand why the ipfw call impacts the flushing of the socket?
-
But it does… try this...
in the LAN enable the captive portal. Make sure that the anti-lockout rule is in place which should allow connectivity to the box even if the captive portal is enabled.
Ssh to the box and spawn a shell... You should be able to get in because the web anti-lock rule allows you in. Fire up the a web browser and try to go to a website... You should see the captive protal login. Login... now logout...
you will notice that your ssh session will die... it will just hang there. The socket is not closed properly to the shell does not see that there has been a disconnect. IP packets to the box are dropped...
you have to do a ~. to disconnect and then you can ssh back to the box.
I believe the same thing is happening to the client browser. If you execute a logout page (not using the popup) which does a POST to index.php you will notice that the browser will just site there waiting... If you run firebug you will see that the HTML logout mesg does make it to the browser but since the socket is never closes and IP packets are just being dropped by the box the browser just goes into a "wait"... This happens in all browsers that I have tested.
Mozilla is nice because it will display the HTML text as it arrives so even though it goes into a wait state the "Disconnecting..." mesg gets displayed. Safari and other webkit friends do not display the text although I have confirmed that they have received it.
I am about to do more testing on this.. My approach is to move the disconnect code to a shell script and then execute it with mwexe_bg and then continue with an exit in index.php. The shell script will sleep for 1 second and then execute the php code to do the disconnect...
I will let you know how that test turns out. If my theory is correct the logout button should start to work properly since the HTML disconnect text is sent to the browser and the socket is closed before ipfw is called to close things off.
--luis
-
and guess what… my theory was correct and the following works beautifully and fixes the logout issue on ** ALL ** browsers including the popup logout window.
Here is what I did... i created the following file in /usr/local/captiveportal/captiveportal-disconnect.php
#!/usr/local/bin/php -f
require_once("functions.inc");
global $g, $config;$sessionid = $argv[1];
$logoutReason = $argv[2];
$term_cause = $argv[3];if ( $argc != 4 || $sessionid == "" || logoutReason == "" || $term_cause == "" )
exit;$cplock = lock('captiveportal');
/* read database */
$cpdb = captiveportal_read_db();$radiusservers = captiveportal_get_radius_servers();
/* find entry */
for ($i = 0; $i < count($cpdb); $i++) {
if ($cpdb[$i][5] == $sessionid) {
captiveportal_disconnect($cpdb[$i],$radiusservers, $term_cause);
captiveportal_logportalauth($cpdb[$i][4],$cpdb[$i][3],$cpdb[$i][2],$logoutReason);
unset($cpdb[$i]);
break;
}
}/* write database */
captiveportal_write_db($cpdb);unlock($cplock);
?>
then I modified disconnect_client in index.php as follows
function disconnect_client($sessionid, $logoutReason = "LOGOUT", $term_cause = 1) {
mwexec_bg("/usr/local/captiveportal/captiveportal-disconnect.php $sessionid $logoutReason $term_cause");
}the time it takes to the shell to spawn php and execute the logout is more than enough time for the HTML Disconnect mesg to make it back to the browser and for httpd to close the socket.
One more thing… its probably better to move the call to disconnect_client in index.php to after the echo of the HTML Disconnect mesg. This way the mesg gets sent to the browser before the disconnect is started. Since mwexec is the very last thing that gets executed before index.php exits there is very little chance that the captive portal disconnect will happen before socket to the client is closed.
Here is the code.
if ($_POST['logout_id']) {
echo << <eod<br><title>Disconnecting…</title>You have been disconnected.
EOD;
disconnect_client($_POST['logout_id']);
exit;The logout is now ** VERY ** clean.
Take care.
–luis</eod<br>
-
I put the client disconnect after the html is sent.
It might need some php buffer flushing too but should be ok for now.Thank you for tracing.
-
I see that all you did was move the call to disconnect_client() to after the html code. In our experimentation here that does nothing for solving the LOGOUT button issue that is discussed in the post. As stated in my postings the HTML is making it to the web browser. So… that is not the issue. Running the console in Chrome, for example, shows that the LOGOUT HTML code made it to the browser. The problem is that the browser does not render the code until after the socket is closed by the server. The socket is never closed because ipfw shuts down the connection and from then on all IP packets are dropped. The connection is left hanging and the browser just spins away waiting for a response from the server. The only way we have found to solve the problem is have the calls to ipfw happen in another process which executes after index.php exits and the sever closes the socket.
So... your code change is pretty much a NOP.
Let me know if you need further info.
Take care.
--luis
-
Try adding
ob_flush();
Just before the disconnect, maybe even
ob_flush(); sleep(1);
To see if that makes a difference.
-
I will try it but i doubt it will make any difference. As stated previously I am seeing the HTML code in the browser. When you do this with mozilla the "Disconnected…." mesg actually is displayed. But in all browsers the page just sits there with the rotating wait i am busy signal waiting for the server to close the socket.
ob_flush() just flushes the output buffers. It does not close the socket. So... it doesn't help given that all of the HMTL code with the "Disconnect..." message has already made it the browser. As stated numerous times the problem with web browsers based in webkit is that these browsers will not render the HTML code until the socket is closed. The socket is Never closed properly because the calls to ipfw are done synchronously before the index.php exits. Once the ipfw calls are made the socket becomes unresponsive and all traffic between the client and pfsense are dropped. The as far as the browser is concerned the socket is open and it continues to wait for data which never arrives. Hence the busy wait signal in ** ALL *** the browsers tested to date.
The only solution found so far is to execute the ipfw commands asynchronously after index.php has exited. As stated previously in this thread that works perfectly...
--luis
-
Can you try putting a closing php://output
Either with fclose() or stream_close(). -
I assume you mean as in
fclose(STDOUT);
that might work. I will give that a shot and report back to you.
–luis
-
Hi have tried…
EOD;
fclose("php://stdin");
fclose("php://stdout");
fclose("php://stderr");
disconnect_client($_POST['logout_id']);and
EOD;
fclose("STDIN");
fclose("STDOUT");
fclose("STDERR");
disconnect_client($_POST['logout_id']);And neither work…
so far the only that works for me is the solution listed earlier in the post where the pfctl statements are done in another process.
--luis
-
how about in the case we discover logout action before calling exit just do
register_shutdown_function(disconnect_client, $_POST['logout_id']);
That should do as well.
-
I will give it a try… the 64,000 question is... does php execute the function before or after it closes the descriptors. If it executes the function before closing the descriptors then it will not work...
--luis
-
This does not work…
if ($_POST['logout_id']) {
echo << <eod<br><title>Disconnecting…</title>You have been disconnected.
EOD;
register_shutdown_function(disconnect_client,$_POST['logout_id']);
exit;
–-As stated before the only thing that I have been able to make work is this...
--- index.php 2011-02-06 16:24:13.000000000 +0000
+++ index.php.new 2011-02-06 16:16:27.000000000 +0000
@@ -412,29 +412,7 @@
*/
function disconnect_client($sessionid, $logoutReason = "LOGOUT", $term_cause = 1) {- global $g, $config;
- $cplock = lock('captiveportal');
- /* read database */
- $cpdb = captiveportal_read_db();
- $radiusservers = captiveportal_get_radius_servers();
- /* find entry */
- for ($i = 0; $i < count($cpdb); $i++) {
- if ($cpdb[$i][5] == $sessionid) {
- captiveportal_disconnect($cpdb[$i],$radiusservers, $term_cause);
- captiveportal_logportalauth($cpdb[$i][4],$cpdb[$i][3],$cpdb[$i][2],$logoutReason);
- unset($cpdb[$i]);
- break;
- }
- }
- /* write database */
- captiveportal_write_db($cpdb);
- unlock($cplock);
- mwexec_bg("/usr/local/captiveportal/captiveportal-disconnect.php $sessionid $logoutReason $term_cause");
}
Where /usr/local/www/captiveportal/captiveportal-disconnect.php contains the following
–-
#!/usr/local/bin/php -f
require_once("functions.inc");
global $g, $config;$sessionid = $argv[1];
$logoutReason = $argv[2];
$term_cause = $argv[3];if ( $argc != 4 || $sessionid == "" || logoutReason == "" || $term_cause == "" )
exit;echo "$sessionid $logoutReason $term_cause";
$cplock = lock('captiveportal');
/* read database */
$cpdb = captiveportal_read_db();$radiusservers = captiveportal_get_radius_servers();
/* find entry */
for ($i = 0; $i < count($cpdb); $i++) {
if ($cpdb[$i][5] == $sessionid) {
captiveportal_disconnect($cpdb[$i],$radiusservers, $term_cause);
captiveportal_logportalauth($cpdb[$i][4],$cpdb[$i][3],$cpdb[$i][2],$logoutReason);
unset($cpdb[$i]);
break;
}
}/* write database */
captiveportal_write_db($cpdb);unlock($cplock);
?>
–-</eod<br>