Is it possible to log out user from a web site if he is using basic authentication?
Killing session is not enough, since, once user is authenticated, each request contains login info, so user is automatically logged in next time he/she access the site using the same credentials.
The only solution so far is to close browser, but that's not acceptable from the usability standpoint.
Have the user click on a link to https://log:out#example.com/. That will overwrite existing credentials with invalid ones; logging them out.
This does so by sending new credentials in the URL. In this case user="log" password="out".
An addition to the answer by bobince ...
With Ajax you can have your 'Logout' link/button wired to a Javascript function. Have this function send the XMLHttpRequest with a bad username and password. This should get back a 401. Then set document.location back to the pre-login page. This way, the user will never see the extra login dialog during logout, nor have to remember to put in bad credentials.
Basic Authentication wasn't designed to manage logging out. You can do it, but not completely automatically.
What you have to do is have the user click a logout link, and send a ‘401 Unauthorized’ in response, using the same realm and at the same URL folder level as the normal 401 you send requesting a login.
They must be directed to input wrong credentials next, eg. a blank username-and-password, and in response you send back a “You have successfully logged out” page. The wrong/blank credentials will then overwrite the previous correct credentials.
In short, the logout script inverts the logic of the login script, only returning the success page if the user isn't passing the right credentials.
The question is whether the somewhat curious “don't enter your password” password box will meet user acceptance. Password managers that try to auto-fill the password can also get in the way here.
Edit to add in response to comment: re-log-in is a slightly different problem (unless you require a two-step logout/login obviously). You have to reject (401) the first attempt to access the relogin link, than accept the second (which presumably has a different username/password). There are a few ways you could do this. One would be to include the current username in the logout link (eg. /relogin?username), and reject when the credentials match the username.
You can do it entirely in JavaScript:
IE has (for a long time) standard API for clearing Basic Authentication cache:
document.execCommand("ClearAuthenticationCache")
Should return true when it works. Returns either false, undefined or blows up on other browsers.
New browsers (as of Dec 2012: Chrome, FireFox, Safari) have "magic" behavior. If they see a successful basic auth request with any bogus other username (let's say logout) they clear the credentials cache and possibly set it for that new bogus user name, which you need to make sure is not a valid user name for viewing content.
Basic example of that is:
var p = window.location.protocol + '//'
// current location must return 200 OK for this GET
window.location = window.location.href.replace(p, p + 'logout:password#')
An "asynchronous" way of doing the above is to do an AJAX call utilizing the logout username. Example:
(function(safeLocation){
var outcome, u, m = "You should be logged out now.";
// IE has a simple solution for it - API:
try { outcome = document.execCommand("ClearAuthenticationCache") }catch(e){}
// Other browsers need a larger solution - AJAX call with special user name - 'logout'.
if (!outcome) {
// Let's create an xmlhttp object
outcome = (function(x){
if (x) {
// the reason we use "random" value for password is
// that browsers cache requests. changing
// password effectively behaves like cache-busing.
x.open("HEAD", safeLocation || location.href, true, "logout", (new Date()).getTime().toString())
x.send("")
// x.abort()
return 1 // this is **speculative** "We are done."
} else {
return
}
})(window.XMLHttpRequest ? new window.XMLHttpRequest() : ( window.ActiveXObject ? new ActiveXObject("Microsoft.XMLHTTP") : u ))
}
if (!outcome) {
m = "Your browser is too old or too weird to support log out functionality. Close all windows and restart the browser."
}
alert(m)
// return !!outcome
})(/*if present URI does not return 200 OK for GET, set some other 200 OK location here*/)
You can make it a bookmarklet too:
javascript:(function (c) {
var a, b = "You should be logged out now.";
try {
a = document.execCommand("ClearAuthenticationCache")
} catch (d) {
}
a || ((a = window.XMLHttpRequest ? new window.XMLHttpRequest : window.ActiveXObject ? new ActiveXObject("Microsoft.XMLHTTP") : void 0) ? (a.open("HEAD", c || location.href, !0, "logout", (new Date).getTime().toString()), a.send(""), a = 1) : a = void 0);
a || (b = "Your browser is too old or too weird to support log out functionality. Close all windows and restart the browser.");
alert(b)
})(/*pass safeLocation here if you need*/);
The following function is confirmed working for Firefox 40, Chrome 44, Opera 31 and IE 11.
Bowser is used for browser detection, jQuery is also used.
- secUrl is the url to a password protected area from which to log out.
- redirUrl is the url to a non password protected area (logout success page).
- you might wish to increase the redirect timer (currently 200ms).
function logout(secUrl, redirUrl) {
if (bowser.msie) {
document.execCommand('ClearAuthenticationCache', 'false');
} else if (bowser.gecko) {
$.ajax({
async: false,
url: secUrl,
type: 'GET',
username: 'logout'
});
} else if (bowser.webkit) {
var xmlhttp = new XMLHttpRequest();
xmlhttp.open("GET", secUrl, true);
xmlhttp.setRequestHeader("Authorization", "Basic logout");
xmlhttp.send();
} else {
alert("Logging out automatically is unsupported for " + bowser.name
+ "\nYou must close the browser to log out.");
}
setTimeout(function () {
window.location.href = redirUrl;
}, 200);
}
Here's a very simple Javascript example using jQuery:
function logout(to_url) {
var out = window.location.href.replace(/:\/\//, '://log:out#');
jQuery.get(out).error(function() {
window.location = to_url;
});
}
This log user out without showing him the browser log-in box again, then redirect him to a logged out page
This isn't directly possible with Basic-Authentication.
There's no mechanism in the HTTP specification for the server to tell the browser to stop sending the credentials that the user already presented.
There are "hacks" (see other answers) typically involving using XMLHttpRequest to send an HTTP request with incorrect credentials to overwrite the ones originally supplied.
Just for the record, there is a new HTTP Response Header called Clear-Site-Data. If your server reply includes a Clear-Site-Data: "cookies" header, then the authentication credentials (not only cookies) should be removed. I tested it on Chrome 77 but this warning shows on the console:
Clear-Site-Data header on 'https://localhost:9443/clear': Cleared data types:
"cookies". Clearing channel IDs and HTTP authentication cache is currently not
supported, as it breaks active network connections.
And the auth credentials aren't removed, so this doesn't works (for now) to implement basic auth logouts, but maybe in the future will. Didn't test on other browsers.
References:
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Clear-Site-Data
https://www.w3.org/TR/clear-site-data/
https://github.com/w3c/webappsec-clear-site-data
https://caniuse.com/#feat=mdn-http_headers_clear-site-data_cookies
It's actually pretty simple.
Just visit the following in your browser and use wrong credentials:
http://username:password#yourdomain.com
That should "log you out".
This is working for IE/Netscape/Chrome :
function ClearAuthentication(LogOffPage)
{
var IsInternetExplorer = false;
try
{
var agt=navigator.userAgent.toLowerCase();
if (agt.indexOf("msie") != -1) { IsInternetExplorer = true; }
}
catch(e)
{
IsInternetExplorer = false;
};
if (IsInternetExplorer)
{
// Logoff Internet Explorer
document.execCommand("ClearAuthenticationCache");
window.location = LogOffPage;
}
else
{
// Logoff every other browsers
$.ajax({
username: 'unknown',
password: 'WrongPassword',
url: './cgi-bin/PrimoCgi',
type: 'GET',
beforeSend: function(xhr)
{
xhr.setRequestHeader("Authorization", "Basic AAAAAAAAAAAAAAAAAAA=");
},
error: function(err)
{
window.location = LogOffPage;
}
});
}
}
$(document).ready(function ()
{
$('#Btn1').click(function ()
{
// Call Clear Authentication
ClearAuthentication("force_logout.html");
});
});
All you need is redirect user on some logout URL and return 401 Unauthorized error on it. On error page (which must be accessible without basic auth) you need to provide a full link to your home page (including scheme and hostname). User will click this link and browser will ask for credentials again.
Example for Nginx:
location /logout {
return 401;
}
error_page 401 /errors/401.html;
location /errors {
auth_basic off;
ssi on;
ssi_types text/html;
alias /home/user/errors;
}
Error page /home/user/errors/401.html:
<!DOCTYPE html>
<p>You're not authorised. Login.</p>
I've just tested the following in Chrome (79), Firefox (71) and Edge (44) and it works fine. It applies the script solution as others noted above.
Just add a "Logout" link and when clicked return the following html
<div>You have been logged out. Redirecting to home...</div>
<script>
var XHR = new XMLHttpRequest();
XHR.open("GET", "/Home/MyProtectedPage", true, "no user", "no password");
XHR.send();
setTimeout(function () {
window.location.href = "/";
}, 3000);
</script>
add this to your application :
#app.route('/logout')
def logout():
return ('Logout', 401, {'WWW-Authenticate': 'Basic realm="Login required"'})
function logout() {
var userAgent = navigator.userAgent.toLowerCase();
if (userAgent.indexOf("msie") != -1) {
document.execCommand("ClearAuthenticationCache", false);
}
xhr_objectCarte = null;
if(window.XMLHttpRequest)
xhr_object = new XMLHttpRequest();
else if(window.ActiveXObject)
xhr_object = new ActiveXObject("Microsoft.XMLHTTP");
else
alert ("Your browser doesn't support XMLHTTPREQUEST");
xhr_object.open ('GET', 'http://yourserver.com/rep/index.php', false, 'username', 'password');
xhr_object.send ("");
xhr_object = null;
document.location = 'http://yourserver.com';
return false;
}
function logout(url){
var str = url.replace("http://", "http://" + new Date().getTime() + "#");
var xmlhttp;
if (window.XMLHttpRequest) xmlhttp=new XMLHttpRequest();
else xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
xmlhttp.onreadystatechange=function()
{
if (xmlhttp.readyState==4) location.reload();
}
xmlhttp.open("GET",str,true);
xmlhttp.setRequestHeader("Authorization","Basic xxxxxxxxxx")
xmlhttp.send();
return false;
}
Based on what I read above I got a simple solution that works on any browser:
1) on you logout page you call an ajax to your login back end. Your login back end must accept logout user. Once the back end accept, the browser clear the current user and assumes the "logout" user.
$.ajax({
async: false,
url: 'http://your_login_backend',
type: 'GET',
username: 'logout'
});
setTimeout(function () {
window.location.href = 'http://normal_index';
}, 200);
2) Now when the user got back to the normal index file it will try to automatic enter in the system with the user "logout", on this second time you must block it by reply with 401 to invoke the login/password dialog.
3) There are many ways to do that, I created two login back ends, one that accepts the logout user and one that doesn't. My normal login page use the one that doesn't accept, my logout page use the one that accepts it.
Sending https://invalid_login#hostname works fine everywhere except Safari on Mac (well, not checked Edge but should work there too).
Logout doesn't work in Safari when a user selects 'remember password' in the HTTP Basic Authentication popup. In this case the password is stored in Keychain Access (Finder > Applications > Utilities > Keychain Access (or CMD+SPACE and type "Keychain Access")). Sending https://invalid_login#hostname doesn't affect Keychain Access, so with this checkbox it is not possible to logout on Safari on Mac. At least it is how it works for me.
MacOS Mojave (10.14.6), Safari 12.1.2.
The code below works fine for me in Firefox (73), Chrome (80) and Safari (12). When a user navigates to a logout page the code is executed and drops the credentials.
//It should return 401, necessary for Safari only
const logoutUrl = 'https://example.com/logout';
const xmlHttp = new XMLHttpRequest();
xmlHttp.open('POST', logoutUrl, true, 'logout');
xmlHttp.send();
Also for some reason Safari doesn't save credentials in the HTTP Basic Authentication popup even when the 'remember password' is selected. The other browsers do this correctly.
This JavaScript must be working for all latest version browsers:
//Detect Browser
var isOpera = !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0;
// Opera 8.0+ (UA detection to detect Blink/v8-powered Opera)
var isFirefox = typeof InstallTrigger !== 'undefined'; // Firefox 1.0+
var isSafari = Object.prototype.toString.call(window.HTMLElement).indexOf('Constructor') > 0;
// At least Safari 3+: "[object HTMLElementConstructor]"
var isChrome = !!window.chrome && !isOpera; // Chrome 1+
var isIE = /*#cc_on!#*/false || !!document.documentMode; // At least IE6
var Host = window.location.host;
//Clear Basic Realm Authentication
if(isIE){
//IE
document.execCommand("ClearAuthenticationCache");
window.location = '/';
}
else if(isSafari)
{//Safari. but this works mostly on all browser except chrome
(function(safeLocation){
var outcome, u, m = "You should be logged out now.";
// IE has a simple solution for it - API:
try { outcome = document.execCommand("ClearAuthenticationCache") }catch(e){}
// Other browsers need a larger solution - AJAX call with special user name - 'logout'.
if (!outcome) {
// Let's create an xmlhttp object
outcome = (function(x){
if (x) {
// the reason we use "random" value for password is
// that browsers cache requests. changing
// password effectively behaves like cache-busing.
x.open("HEAD", safeLocation || location.href, true, "logout", (new Date()).getTime().toString())
x.send("");
// x.abort()
return 1 // this is **speculative** "We are done."
} else {
return
}
})(window.XMLHttpRequest ? new window.XMLHttpRequest() : ( window.ActiveXObject ? new ActiveXObject("Microsoft.XMLHTTP") : u ))
}
if (!outcome) {
m = "Your browser is too old or too weird to support log out functionality. Close all windows and restart the browser."
}
alert(m);
window.location = '/';
// return !!outcome
})(/*if present URI does not return 200 OK for GET, set some other 200 OK location here*/)
}
else{
//Firefox,Chrome
window.location = 'http://log:out#'+Host+'/';
}
type chrome://restart in the address bar and chrome, with all its apps that are running in background, will restart and the Auth password cache will be cleaned.
use a session ID (cookie)
invalidate the session ID on the server
Don't accept users with invalid session IDs
I updated mthoring's solution for modern Chrome versions:
function logout(secUrl, redirUrl) {
if (bowser.msie) {
document.execCommand('ClearAuthenticationCache', 'false');
} else if (bowser.gecko) {
$.ajax({
async: false,
url: secUrl,
type: 'GET',
username: 'logout'
});
} else if (bowser.webkit || bowser.chrome) {
var xmlhttp = new XMLHttpRequest();
xmlhttp.open(\"GET\", secUrl, true);
xmlhttp.setRequestHeader(\"Authorization\", \"Basic logout\");\
xmlhttp.send();
} else {
// http://stackoverflow.com/questions/5957822/how-to-clear-basic-authentication-details-in-chrome
redirUrl = url.replace('http://', 'http://' + new Date().getTime() + '#');
}
setTimeout(function () {
window.location.href = redirUrl;
}, 200);
}
As others have said, we need to get the same URL and send an error (e.g., 401: StatusUnauthorized something like that), and that's it.
And I use the Get method to let it know I need to logout,
Here is a full example of writing with golang.
package main
import (
"crypto/subtle"
"fmt"
"log"
"net/http"
)
func BasicAuth(username, password, realm string, handlerFunc http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
queryMap := r.URL.Query()
if _, ok := queryMap["logout"]; ok { // localhost:8080/public/?logout
w.WriteHeader(http.StatusUnauthorized) // 401
_, _ = w.Write([]byte("Success logout!\n"))
return
}
user, pass, ok := r.BasicAuth()
if !ok ||
subtle.ConstantTimeCompare([]byte(user), []byte(username)) != 1 ||
subtle.ConstantTimeCompare([]byte(pass), []byte(password)) != 1 {
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/WWW-Authenticate
w.Header().Set("WWW-Authenticate", `Basic realm="`+realm+`", charset="UTF-8"`)
w.WriteHeader(http.StatusUnauthorized)
_, _ = w.Write([]byte("Unauthorised.\n"))
return
}
handlerFunc(w, r)
}
}
type UserInfo struct {
name string
psw string
}
func main() {
portNumber := "8080"
guest := UserInfo{"guest", "123"}
// localhost:8080/public/ -> ./public/everyone
publicHandler := http.StripPrefix(
"/public/", http.FileServer(http.Dir("./public/everyone")),
)
publicHandlerFunc := func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
publicHandler.ServeHTTP(w, r)
/*
case http.MethodPost:
case http.MethodPut:
case http.MethodDelete:
*/
default:
return
}
}
http.HandleFunc("/public/",
BasicAuth(guest.name, guest.psw, "Please enter your username and password for this site",
publicHandlerFunc),
)
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", portNumber), nil))
}
When you have already logout, then you need to refresh (F5) the page. Otherwise, you may see the old content.
Actually I think basic authentication was intended to be used with static pages, not for any sophisticated session management or CGI pages.
Thus when wanting session management you should design a classic "login form" to query for user and password (maybe 2nd factor as well).
The CGI form handler should convert successful authentication to a session (ID) that is remembered on the server and (in a cookie or as part of the URI).
Then logout can be implemented simply by making the server (and client) "forget" the session.
The other advantage is that (even when encrypted) the user and password is not send with every request to the server (instead the session ID would be sent).
If the session ID on the server is combined with a timestamp for the "last action" performed, then session timeout could be implemented by comparing that timestamp with the current time:
If the time span is too large, "timeout" the session by forgetting the session ID.
Any request to an invalid session would cause a redirection to the login page (or maybe if you want to make it more comfortable, you can have a "revalidation form" that requests the password again, too).
As a proof of concept I had implemented a completely cookie-free session management that is purely URI-based (the session ID is always part of the URI).
However the complete code would be too long for this answer.
Special care about performance has to be taken when wanting to handle several thousands of concurrent sessions.
For anyone who use Windows Authentication (also known as Negotiate, Kerberos, or NTLM authentication), I use ASP.NET Core with Angular.
I found an efficient manner to change users !
I modify my login method on the javascript side like that :
protected login(changeUser: boolean = false): Observable<AuthInfo> {
let params = new HttpParams();
if(changeUser) {
let dateNow = this.datePipe.transform(new Date(), 'yyyy-MM-dd HH:mm:ss');
params = params.set('changeUser', dateNow!);
}
const url: string = `${environment.yourAppsApiUrl}/Auth/login`;
return this.http.get<AuthInfo>(url, { params: params });
}
Here is my method on the backend :
[Route("api/[controller]")]
[ApiController]
[Produces("application/json")]
[Authorize(AuthenticationSchemes = NegotiateDefaults.AuthenticationScheme)]
public class AuthController : Controller
{
[HttpGet("login")]
public async Task<IActionResult> Login(DateTime? changeUser = null)
{
if (changeUser > DateTime.Now.AddSeconds(-3))
return Unauthorized();
...
... (login process)
...
return Ok(await _authService.GetToken());
}
}
return Unauthorized() return the 401 code that causes the browser identification popup window to appear, here is the process :
I transmit the date now as a parameter if I want to change user.
I return the 401 code if no more than 3 seconds have passed since that moment Now.
I complete my credential and the same request with the same parameter is sent to the backend.
Since more than 3 seconds have passed, I continue the login process but this time with the new credential !
This is how my logout is working using form:
create basic auth user logout with password logout
create folder logout/ and add .htaccess: with line 'require user logout'
RewriteEngine On
AuthType Basic
AuthName "Login"
AuthUserFile /mypath/.htpasswd
require user logout
add logout button to website as form like:
<form action="https://logout:logout#example.com/logout/" method="post">
<button type="submit">Logout</button>
</form>
logout/index.php could be something like:
<?php
echo "LOGOUT SUCCESS";
header( "refresh:2; url=https://example.com" );
?>
5.9.2022 confirmed working on chrome, edge and samsung android internet browser
function logout(secUrl, redirUrl) {
if (bowser.msie) {
document.execCommand('ClearAuthenticationCache', 'false');
} else if (bowser.gecko) {
$.ajax({
async: false,
url: secUrl,
type: 'GET',
username: 'logout'
});
} else if (bowser.webkit) {
var xmlhttp = new XMLHttpRequest();
xmlhttp.open("GET", secUrl, true);
xmlhttp.setRequestHeader("Authorization", "Basic logout");
xmlhttp.send();
} else {
alert("Logging out automatically is unsupported for " + bowser.name
+ "\nYou must close the browser to log out.");
}
setTimeout(function () {
window.location.href = redirUrl;
}, 200);
}
I tried using the above in the following way.
?php
ob_start();
session_start();
require_once 'dbconnect.php';
// if session is not set this will redirect to login page
if( !isset($_SESSION['user']) ) {
header("Location: index.php");
exit;
}
// select loggedin users detail
$res=mysql_query("SELECT * FROM users WHERE userId=".$_SESSION['user']);
$userRow=mysql_fetch_array($res);
?>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Welcome - <?php echo $userRow['userEmail']; ?></title>
<link rel="stylesheet" href="assets/css/bootstrap.min.css" type="text/css" />
<link rel="stylesheet" href="style.css" type="text/css" />
<script src="assets/js/bowser.min.js"></script>
<script>
//function logout(secUrl, redirUrl)
//bowser = require('bowser');
function logout(secUrl, redirUrl) {
alert(redirUrl);
if (bowser.msie) {
document.execCommand('ClearAuthenticationCache', 'false');
} else if (bowser.gecko) {
$.ajax({
async: false,
url: secUrl,
type: 'GET',
username: 'logout'
});
} else if (bowser.webkit) {
var xmlhttp = new XMLHttpRequest();
xmlhttp.open("GET", secUrl, true);
xmlhttp.setRequestHeader("Authorization", "Basic logout");
xmlhttp.send();
} else {
alert("Logging out automatically is unsupported for " + bowser.name
+ "\nYou must close the browser to log out.");
}
window.location.assign(redirUrl);
/*setTimeout(function () {
window.location.href = redirUrl;
}, 200);*/
}
function f1()
{
alert("f1 called");
//form validation that recalls the page showing with supplied inputs.
}
</script>
</head>
<body>
<nav class="navbar navbar-default navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar" aria-expanded="false" aria-controls="navbar">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="http://www.codingcage.com">Coding Cage</a>
</div>
<div id="navbar" class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li class="active">Back to Article</li>
<li>jQuery</li>
<li>PHP</li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
<span class="glyphicon glyphicon-user"></span> Hi' <?php echo $userRow['userEmail']; ?> <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><span class="glyphicon glyphicon-log-out"></span> Sign Out</li>
</ul>
</li>
</ul>
</div><!--/.nav-collapse -->
</div>
</nav>
<div id="wrapper">
<div class="container">
<div class="page-header">
<h3>Coding Cage - Programming Blog</h3>
</div>
<div class="row">
<div class="col-lg-12" id="div_logout">
<h1 onclick="logout(window.location.href, 'www.espncricinfo.com')">MichaelA1S1! Click here to see log out functionality upon click inside div</h1>
</div>
</div>
</div>
</div>
<script src="assets/jquery-1.11.3-jquery.min.js"></script>
<script src="assets/js/bootstrap.min.js"></script>
</body>
</html>
<?php ob_end_flush(); ?>
But it only redirects you to new location. No logout.
I am attempting to set up a websocket with server running on my raspberry pi. The following code is slightly modified from an example I found here.
I have built a whole webpage around this example allowing me to control gpio and send messages to a serial device plugged into the pi. Both that site and this example work perfectly from my laptop (windows 10 using Chrome or Firefox).
However when I connect from my phone (Android 5.0.1 using Chrome for android). It appears to never open the socket. In the example code it just displays "messages go here.
My first thought was the chrome on android didn't support websockets but I was able to connect and echo messages on this site http://www.websocket.org/echo.html. So it appears the functionality is there.
What else would prevent the socket from opening ?
pysocket.py
import tornado.httpserver
import tornado.websocket
import tornado.ioloop
import tornado.web
class WSHandler(tornado.websocket.WebSocketHandler):
def check_origin(self, origin):
return True
def open(self):
print 'New connection was opened'
self.write_message("Welcome to my websocket!")
def on_message(self, message):
print 'Incoming message:', message
self.write_message("You said: " + message)
def on_close(self):
print 'Connection was closed...'
application = tornado.web.Application([
(r'/ws', WSHandler),
])
if __name__ == "__main__":
http_server = tornado.httpserver.HTTPServer(application)
http_server.listen(8888)
tornado.ioloop.IOLoop.instance().start()
pysocket.php
<!doctype html>
<html>
<head>
<title>WebSockets with Python & Tornado</title>
<meta charset="utf-8" />
<style type="text/css">
body {
text-align: center;
min-width: 500px;
}
</style>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
<script>
$(function(){
var ws;
var logger = function(msg){
var now = new Date();
var sec = now.getSeconds();
var min = now.getMinutes();
var hr = now.getHours();
$("#log").html($("#log").html() + "<br/>" + hr + ":" + min + ":" + sec + " ___ " + msg);
//$("#log").animate({ scrollTop: $('#log')[0].scrollHeight}, 100);
$('#log').scrollTop($('#log')[0].scrollHeight);
}
var sender = function() {
var msg = $("#msg").val();
if (msg.length > 0)
ws.send(msg);
$("#msg").val(msg);
}
ws = new WebSocket("ws://raspberrypi-mike:8888/ws");
ws.onmessage = function(evt) {
logger(evt.data);
};
ws.onclose = function(evt) {
$("#log").text("Connection was closed...");
$("#thebutton #msg").prop('disabled', true);
};
ws.onopen = function(evt) { $("#log").text("Opening socket..."); };
$("#msg").keypress(function(event) {
if (event.which == 13) {
sender();
}
});
$("#thebutton").click(function(){
sender();
});
});
</script>
</head>
<body>
<h1>WebSockets with Python & Tornado</h1>
<div id="log" style="overflow:scroll;width:500px; height:200px;background-color:#ffeeaa; margin:auto; text-align:left">Messages go here</div>
<div style="margin:10px">
<input type="text" id="msg" style="background:#fff;width:200px"/>
<input type="button" id="thebutton" value="Send" />
</div>
www.LowPowerLab.com
</body>
</html>
ADDED SOME EXTRA CODE
I would recommend using node.js for that:
var express = require("express");
var app = express();
var http = require("http").Server(app);
var path = require("path");
var io = require('socket.io')(http);
var SerialPort = require('serialport');
var gpio = require('rpio');
http.listen(3000);
var serialPort = new SerialPort.SerialPort("/dev/ttyAMA0", {
baudrate: 115200,
dataBits: 8,
parity: "none",
stopBits: 1,
flowControl: false
});
io.on('connection', function(socket){
console.log('Connected');
socket.on("WriteSerial:get",function(data){
var hex = new Buffer (data, "hex"); //be careful passing data
writeSerial(serialPort, hex);
io.emit("WriteSerial:response", "Data writen!");
});
socket.on("ReadGPIO:get",function(data){
var input = readPin(data.pin);
io.emit("ReadGPIO:response", input);
});
socket.on("WriteGPIO:get",function(data){
writePin(data.pin, data.time);
io.emit("WriteGPIO:response", "Set!");
});
socket.on("unWriteGPIO:get",function(data){
unwritePin(data);
io.emit("unWriteGPIO:response", "Set!");
});
}
app.use(express.static(path.join(__dirname, '/')));
app.get("/home",function(req,res,next){
res.sendFile(path.join(__dirname + "/index.html"));
});
function writeSerial (port, data) {
port.write(data, function(err) {
if (err) {
return console.log('Error on write: ', err.message);
} else {
console.log('Data written: ' + data);
}
});
}
function readPin(pin){
rpio.open(pin, rpio.INPUT);
var read = rpio.read(pin) ? 'high' : 'low';
return read;
}
function writePin(pin, timeInMs){
rpio.open(pin, rpio.OUTPUT, rpio.LOW);
rpio.write(pin, rpio.HIGH);
if (timeInMs > 0) {
setTimeout(function(){
rpio.write(pin, rpio.LOW);
}, timeInMs);
} //You can put 0 if You want it to be high until You shut it down
}
function unWritePin(pin){
if(readPin(pin) === 'high') {
rpio.write(pin, rpio.LOW);
} else {
console.log("Pin already low!");
}
}
Be sure You have instaled the node.js with right version. If not do this in terminal:
sudo apt-get remove nodered && sudo apt-get remove nodejs nodejs-legacy &&
curl -sL https://deb.nodesource.com/setup_4.x | sudo bash - && sudo
apt-get install -y nodejs
Make a folder 'server' in '/home/pi/', add server.js to it. Add code I provided to server.js. Open that folder with terminal:
cd /home/pi/server/
Afther that install all modules used on server:
sudo npm install express && sudo npm install http && sudo npm install path &&
sudo npm install socket.io && sudo npm install serialport --unsafe-perm &&
sudo npm install rpio --unsafe-perm
Now all we have to do is create client side part. In folder '/home/pi/server' create index.html file and add folder called 'js'. In folder 'js' add socket.io.js for client side which You can find in folder '/home/pi/server/node_modules/socket.io/node_modules/socket.io-client/'.
Include socket.io.js for client side into Your index.html like this:
<script type="text/javascript" src="js/socket.io.js" /></script>
Also add main.js file to 'js' folder where You will put Your javascript code in and include it to index.html:
<script type="text/javascript" src="js/main.js" /></script>
<script type="text/javascript" src="js/jquery.js" /></script>
I will not make any graphics for but some main.js code is here:
$(document).ready(function() {
var socket = io.connect('http://your_ip_address_rpi:3000');
$( "#myButton" ).click(function(){
io.emit("WriteSerial:get", $("#myTextHolder").val()); //"FAAF531C" this is string of hex, should be added some filter to pass error when char is not part of HEX!
});
$( "#myButton2" ).click(function(){
io.emit("WriteGPIO:get", {"pin" : $("#myPinHolder").val(), "time" : $("#myTimeHolder").val()})
}
To run server on RPI startup add 'sudo node /home/pi/server/server.js &' to '/etc/rc.local' before 'exit 0' with 'sudo nano' editor.
It will work really good on any device.
For the hostnames of the devices to work across network, a device has to advertise its own hostname or just responding to DNS queries for its own hostname.
Whatever implementation the Raspberry Pi is using, your laptop is supporting it, but your phone isn't.
So, to be able to connect, you need to change your hostname raspberrypi-mike to your Raspberry Pi's IP address, inside your JavaScript code.
I have a dynamically created table with data from a django model. This table is displaying additional information about each data_element.
In the last column there should either
be a button displayed for each row, which will run the script with additional keywords from that specific data_element, without reloading or freezing the page.
If the script is still running (can take hours) there should be progress icon displayed and
if the script has already finished, there should be a button displayed, redirecting to an results.html
How can I program that with django?
Currently I am executing a script manually, but for that I am redirecting to another template with the args to parse and when the script is executed (with call_command('my_script', *args) the page freezes until the script ends.
<form action="{% url 'calculate' element_id %}">
<input class="btn btn-primary-custom" id="submit" type="submit" value="run script">
</form>
I tried to insert the code from this post:
Django button ajax click
But when I click on that button, nothing happens. What do I have to do, to create that table?
EDIT
the function for my button currently looks like this:
$(document).ready(function(){
$('.calculate-btn').bind('click', function(){
function getCookie(name) {
var cookieValue = null;
if (document.cookie && document.cookie != '') {
var cookies = document.cookie.split(';');
for (var i = 0; i < cookies.length; i++) {
var cookie = jQuery.trim(cookies[i]);
// Does this cookie string begin with the name we want?
if (cookie.substring(0, name.length + 1) == (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
var btn-data= $(this).attr('btn-data');
var csrftoken = getCookie('csrftoken');
$.ajax({
type: 'POST',
url : "/run/",
dataType: "html",
data : {'csrfmiddlewaretoken': csrftoken, 'btn-data':btn-data},
success: function(data, status, xhr){
console.log("SUCCESS")
},
error: function(data, status, xhr){
console.log("ERROR")
}
});
return false;
});
});
and my view gets called from a button click:
<input id="{{item.id}}" class='calculate-btn' name="update_log" type="button" value="Run Script" btn-data={{ item.id }}>
How can I now dynamically change the button, while the script is still running?
You will need a way to launch async tasks, and a way to get the task status, and then add quite some js/ajax code to launch the task and update your table accordingly.
For the first two parts, the canonical solution is celery.
Another, less recommended way to do that (less recommended, but probably easier to setup than celery) is to simply spawn a child, detached process which will complete the job in the background and store the status and results somewhere (e.g. in your database or in a memory storage like redis).
The main disadventage is that you lose control of what is happening in the child process. Depending on your project - if you don't care about the process control - it may turn out to be better solution.
I am just getting started into python and flask (for the raspberry pi). I want a web application that would execute some python code to pan and tilt a camera and display a video stream.
My code up until now for flask is:
from flask import Flask, render_template
import time
import serial
#ser = serial.Serial('/dev/ttyUSB0',9600)
app = Flask(__name__)
#app.route('/')
#app.route('/<cmd>') #each button in my html redirects to a specified directory
def execute(cmd=None):
if cmd == "down":
print "Moving Down"
#ser.write("D")
if cmd == "up":
print "Moving Up"
#ser.write("U")
if cmd == "left":
print "Moving Left"
# ser.write("L")
if cmd == "right":
print "Moving Right"
#ser.write("R")
if cmd == "reset":
print "Reseting.."
#ser.write("X")
return render_template("main.html")
if __name__ == "__main__":
app.run(host='0.0.0.0', port=8080, debug=True)
The problem is my code relies on the each button redirecting to a new directory, while this does work well, it refreshes the page each time which means my embedded video reloads and buffers again. Is there a better way of detecting a button press and then executing python code using flask?
I would split it out into two routes to make it easier to see what you have to do:
LEFT, RIGHT, UP, DOWN, RESET = "left", "right", "up", "down", "reset"
AVAILABLE_COMMANDS = {
'Left': LEFT,
'Right': RIGHT,
'Up': UP,
'Down': DOWN,
'Reset': RESET
}
#app.route('/')
def execute():
return render_template('main.html', commands=AVAILABLE_COMMANDS)
#app.route('/<cmd>')
def command(cmd=None):
if cmd == RESET:
camera_command = "X"
response = "Resetting ..."
else:
camera_command = cmd[0].upper()
response = "Moving {}".format(cmd.capitalize())
# ser.write(camera_command)
return response, 200, {'Content-Type': 'text/plain'}
Then in your template you just need to use some JavaScript to send off the request:
{# in main.html #}
{% for label, command in commands.items() %}
<button class="command command-{{ command }}" value="{{ command }}">
{{ label }}
</button>
{% endfor %}
{# and then elsewhere #}
<script>
// Only run what comes next *after* the page has loaded
addEventListener("DOMContentLoaded", function() {
// Grab all of the elements with a class of command
// (which all of the buttons we just created have)
var commandButtons = document.querySelectorAll(".command");
for (var i=0, l=commandButtons.length; i<l; i++) {
var button = commandButtons[i];
// For each button, listen for the "click" event
button.addEventListener("click", function(e) {
// When a click happens, stop the button
// from submitting our form (if we have one)
e.preventDefault();
var clickedButton = e.target;
var command = clickedButton.value;
// Now we need to send the data to our server
// without reloading the page - this is the domain of
// AJAX (Asynchronous JavaScript And XML)
// We will create a new request object
// and set up a handler for the response
var request = new XMLHttpRequest();
request.onload = function() {
// We could do more interesting things with the response
// or, we could ignore it entirely
alert(request.responseText);
};
// We point the request at the appropriate command
request.open("GET", "/" + command, true);
// and then we send it off
request.send();
});
}
}, true);
</script>
I've got the same problem, and the answer is simple using ajax XmlHttpRequest:
// send a request, but don't refresh page
xhttp = new XMLHttpRequest();
xhttp.open("GET", "your script action", true);
xhttp.send();
Here's a small example, calling current script with parameters "like", embedded in a function:
function likeStuffs()
{
// send a request, but don't refresh page
xhttp = new XMLHttpRequest();
xhttp.open("GET", "?like", true);
xhttp.send();
}
You can simply do this with help of AJAX... Here is a example which calls a python function which prints hello without redirecting or refreshing the page.
In app.py put below code segment.
//rendering the HTML page which has the button
#app.route('/json')
def json():
return render_template('json.html')
//background process happening without any refreshing
#app.route('/background_process_test')
def background_process_test():
print "Hello"
return "nothing"
And your json.html page should look like below.
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<script type=text/javascript>
$(function() {
$('a#test').bind('click', function() {
$.getJSON('/background_process_test',
function(data) {
//do nothing
});
return false;
});
});
</script>
//button
<div class='container'>
<h3>Test</h3>
<form>
<a href=# id=test><button class='btn btn-default'>Test</button></a>
</form>
</div>
Here when you press the button Test simple in the console you can see "Hello" is displaying without any refreshing.