Session auto logout after inactivity - node.js

Is there a built in feature in express session, to enable auto logout after given time of inactivity ? I am using it as below, and want it to logout if session is inactive for half an hour.
app.use(session({
key: 'sessid',
secret: 'This is secret',
resave: true,
saveUninitialized: true,
store: new RedisStore(redisOptions),
cookie: {
path: '/',
httpOnly: true,
secure: false,
maxAge: 24 * 60 * 60 * 1000,
signed: false
}
}))

Ok, I'll throw my two cents into the ring here.
Even though it's in theory possible to implement this using rolling session, I don't think you should...
It would require each user action the send a request to the server, in order for the user not to be logged out.
You miss an opportunity to inform your user that he/she will be logged out automatically soon (this is what the banks do, for example).
#Seth did point out in a comment above that there is actually a way to remedy this: "If the front end is separate from the server, you could have client side routing middleware that checks the cookie and visually logs you out, thus proving a good UX."
I think this is clever, but I also think it's like putting lipstick on a pig.
I believe that the best approach here is to handle this on the client side.
I would suggest something like this:
var AutoLogout = (function() {
function AutoLogout() {
this.events = ['load', 'mousemove', 'mousedown',
'click', 'scroll', 'keypress'];
this.warn = this.warn.bind(this);
this.logout = this.logout.bind(this);
this.resetTimeout = this.resetTimeout.bind(this);
var self = this;
this.events.forEach(function(event) {
window.addEventListener(event, self.resetTimeout);
});
this.setTimeout();
}
var _p = AutoLogout.prototype;
_p.clearTimeout = function() {
if(this.warnTimeout)
clearTimeout(this.warnTimeout);
if(this.logoutTimeout)
clearTimeout(this.logoutTimeout);
};
_p.setTimeout = function() {
this.warnTimeout = setTimeout(this.warn, 29 * 60 * 1000);
this.logoutTimeout = setTimeout(this.logout, 30 * 60 * 1000);
};
_p.resetTimeout = function() {
this.clearTimeout();
this.setTimeout();
};
_p.warn = function() {
alert('You will be logged out automatically in 1 minute.');
};
_p.logout = function() {
// Send a logout request to the API
console.log('Sending a logout request to the API...');
this.destroy(); // Cleanup
};
_p.destroy = function() {
this.clearTimeout();
var self = this;
this.forEach(function(event) {
window.removeEventListener(event, self.resetTimeout);
});
};
return AutoLogout;
})();
es2015
class AutoLogout {
constructor() {
this.events = ['load', 'mousemove', 'mousedown',
'click', 'scroll', 'keypress'];
this.warn = this.warn.bind(this);
this.logout = this.logout.bind(this);
this.resetTimeout = this.resetTimeout.bind(this);
this.events.forEach((event) => {
window.addEventListener(event, this.resetTimeout);
});
this.setTimeout();
}
clearTimeout() {
if(this.warnTimeout)
clearTimeout(this.warnTimeout);
if(this.logoutTimeout)
clearTimeout(this.logoutTimeout);
}
setTimeout() {
this.warnTimeout = setTimeout(this.warn, 29 * 60 * 1000);
this.logoutTimeout = setTimeout(this.logout, 30 * 60 * 1000);
}
resetTimeout() {
this.clearTimeout();
this.setTimeout();
}
warn() {
alert('You will be logged out automatically in 1 minute.');
}
logout() {
// Send a logout request to the API
console.log('Sending a logout request to the API...');
this.destroy(); // Cleanup
}
destroy() {
this.clearTimeout();
this.events.forEach((event) => {
window.removeEventListener(event, this.resetTimeout);
});
}
}
Partial polling solution:
var activityPolling = (function() {
var events = ['load', 'mousemove', 'mousedown', 'click', 'scroll', 'keypress'];
var active = true;
var timeout;
function poll() {
if(active) {
console.log('polling the server...')
}
}
function setIdle() {
active = false;
}
function setActive() {
active = true;
if(timeout)
clearTimeout(timeout);
timeout = setTimeout(setIdle, 30 * 60 * 1000);
}
function destroy() {
clearInterval(interval);
events.forEach(function(event) {
window.removeEventListener(event, setActive);
});
}
events.forEach(function(event) {
window.addEventListener(event, setActive);
});
setActive();
var interval = setInterval(poll, 60 * 1000);
return {
interval: interval,
destroy: destroy
}
})();

Rolling session may solve the purpose.
If you use the "rolling" option for session to "true," it will update the session timeout on new requests.
What you can do is: set max-age to 5 minutes.
maxAge: 30*10000
When there is no activity max-age will destroy the session.
However, when there is any activity, rolling will renew the session to be alive for next 30 minutes.
Again, the word in-activity in this question is little misleading.
In-activity could be any (or all) of no-mouse-movement, no-mouse-click, or no-interaction-with-server.
If you refer inactivity as no-interaction-with-server, this logic will work. However for no-ui-interactions inactivity, you need to handle from client side

Related

Device shadow state not changes

Trying to create aws-iot device by using manual. I did all required settings while creating Pi3-DHT11-Node device. Simplified code below:
var awsIot = require('aws-iot-device-sdk');
const NODE_ID = 'Pi3-DHT11-Noded';
const INIT_DELAY = 15;
const TAG = '[' + NODE_ID + '] >>>>>>>>> ';
var thingShadow = awsIot.thingShadow({
keyPath: './certs_p/fea2f8efae7-private.pem.key',
certPath: './certs_p/fea2f8efae7-certificate.pem.crt',
caPath: './certs_p/AmazonRootCA1.pem',
clientId: NODE_ID,
host: 'a3cnel9blokzm0-ats.iot.eu-west-3.amazonaws.com',
port: 8883,
region: 'eu-west-3',
debug: true, // optional to see logs on console
});
thingShadow.on('connect', function() {
console.log(TAG, 'Connected.');
thingShadow.register(NODE_ID, {}, function() {
console.log(TAG, 'Registered.');
console.log(TAG, 'Reading data in ' + INIT_DELAY + ' seconds.');
setTimeout(sendData, INIT_DELAY * 1000); // wait for `INIT_DELAY` seconds before reading the first record
});
});
function sendData() {
var DHT11State = {
"state": {
"desired":
{
"temp": 20,
"humd": 33
}
}
};
var clientTokenUpdate = thingShadow.update(NODE_ID, DHT11State);
if (clientTokenUpdate === null) {
console.log(TAG, 'Shadow update failed, operation still in progress');
} else {
console.log(TAG, 'Shadow update success.');
}
// keep sending the data every 30 seconds
console.log(TAG, 'Reading data again in 30 seconds.');
setTimeout(sendData, 30000); // 30,000 ms => 30 seconds
}
Script is working fine, but this does not change shadow state. Why? How to fix that?
It is most likely that you do not have the device setup to sync the shadow to the cloud.
Follow the steps in this part of the tutorial, and ensure that you click on "Sync to the Cloud" as per Step 2, then redeploy your Greengrass Group.
https://docs.aws.amazon.com/greengrass/latest/developerguide/comms-enabled.html

yii-node socket not working on Google Compute Engine

I have configured yii-node socket in Yii 1.1 and everything works fine on localhost. No issues at all.
Now that I am trying to host my project on Google Compute Engine, I was having issues binding external IP to internal IP in order to start NodeJs server which was sending 404 on socket.io.js. I was able to fix it by removing the host from server.listen method so that it can bind to any IP address and socket.io js file is loaded to the client with it's public IP. I have created a reserved static IP for our instance and used the same IP in the below configuration.
Client handshake to server is not working since the uinque url which is generated is still pointing to localhost on GCP when website is loaded. I believe it should be server-ip instead of localhost.
My Node-Socket configuration is
'nodeSocket' => array(
'class' => 'application.extensions.yii-node-socket.lib.php.NodeSocket',
'host' => 'xxx.211.xxx.99', // default is 127.0.0.1, can be ip or domain name, without http
'allowedServerAddresses' => [ "127.0.0.1", "xxx.211.xxx.99", "::ffff:xxx.211.xxx.99" ],
'origin' => '*:*',
'port' => 3001 // default is 3001, should be integer )
Below is the log when I try to fire events from Yii
debug - served static content /socket.io.js
debug - client authorized
info - handshake authorized nvQHUiA9E1dBBKmHv69k
debug - setting request GET /socket.io/1/websocket/nvQHUiA9E1dBBKmHv69k
debug - set heartbeat interval for client nvQHUiA9E1dBBKmHv69k
debug - client authorized for
debug - websocket writing 1::
debug - client authorized for /server
debug - websocket writing 1::/server
info - transport end (undefined)
debug - set close timeout for client nvQHUiA9E1dBBKmHv69k
debug - cleared close timeout for client nvQHUiA9E1dBBKmHv69k
debug - cleared heartbeat interval for client nvQHUiA9E1dBBKmHv69k
debug - discarding transport
server.js code
var express = require('express');
var app = express();
var server = require('http').createServer(app)
var io = require('socket.io').listen(server);
var cookie = require('cookie');
var serverConfiguration = require('./server.config.js');
var storeProvider = express.session.MemoryStore;
var sessionStorage = new storeProvider();
var componentManager = require('./components/component.manager.js');
componentManager.set('config', serverConfiguration);
var eventManager = require('./components/event.manager.js');
var socketPull = require('./components/socket.pull.js');
var db = require('./components/db.js');
db.init(serverConfiguration.dbOptions);
componentManager.set('db', db);
componentManager.set('sp', socketPull);
componentManager.set('io', io);
componentManager.set('eventManager', eventManager);
componentManager.set('sessionStorage', sessionStorage);
server.listen(serverConfiguration.port);
console.log('Listening ' + serverConfiguration.host + ':' + serverConfiguration.port);
// accept all connections from local server
if (serverConfiguration.checkClientOrigin) {
console.log('Set origin: ' + serverConfiguration.origin);
io.set("origins", serverConfiguration.origin);
}
// client
io.of('/client').authorization(function (handshakeData,accept) {
if (!handshakeData.headers.cookie) {
return accept('NO COOKIE TRANSMITTED', false);
}
handshakeData.cookie = cookie.parse(handshakeData.headers.cookie);
var sid = handshakeData.cookie[serverConfiguration.sessionVarName];
if (!sid) {
return accept('Have no session id', false);
}
handshakeData.sid = sid;
handshakeData.uid = null;
// create write method
handshakeData.writeSession = function (fn) {
sessionStorage.set(sid, handshakeData.session, function () {
if (fn) {
fn();
}
});
};
// trying to get session
sessionStorage.get(sid, function (err, session) {
// create session handler
var createSession = function () {
var sessionData = {
sid : sid,
cookie : handshakeData.cookie,
user : {
role : 'guest',
id : null,
isAuthenticated : false
}
};
// store session in session storage
sessionStorage.set(sid, sessionData, function () {
// authenticate and authorise client
handshakeData.session = sessionData;
accept(null, true);
});
};
// check on errors or empty session
if (err || !session) {
if (!session) {
// create new session
createSession();
} else {
// not authorise client if errors occurred
accept('ERROR: ' + err, false);
}
} else {
if (!session) {
createSession();
} else {
// authorize client
handshakeData.session = session;
handshakeData.uid = session.user.id;
accept(null, true);
}
}
});
}).on('connection', function (socket) {
// add socket to pull
socketPull.add(socket);
// connect socket to him channels
componentManager.get('channel').attachToChannels(socket);
// bind events to socket
eventManager.client.bind(socket);
});
// server
io.of('/server').authorization(function (data, accept) {
if (data && data.address) {
if (data.headers['cookie']) {
data.cookie = cookie.parse(data.headers.cookie);
if (data.cookie.PHPSESSID) {
data.sid = data.cookie.PHPSESSID;
var found = false;
for (var i in serverConfiguration.allowedServers) {
if (serverConfiguration.allowedServers[i] == data.address.address) {
found = true;
break;
}
}
if (found) {
var createSession = function () {
var sessionData = {
sid : data.cookie.PHPSESSID,
cookie : data.cookie,
user : {
role : 'guest',
id : null,
isAuthenticated : false
}
};
// store session in session storage
sessionStorage.set(data.cookie.PHPSESSID, sessionData, function () {
// authenticate and authorise client
data.session = sessionData;
accept(null, true);
});
};
data.writeSession = function (fn) {
sessionStorage.set(data.cookie.PHPSESSID, data.session, function () {
if (fn) {
fn();
}
});
};
sessionStorage.get(data.cookie.PHPSESSID, function (err, session) {
if (err || !session) {
if (!session) {
createSession();
} else {
accept('ERROR: ' + err, false);
}
} else {
if (!session) {
createSession();
} else {
// authorize client
data.session = session;
data.uid = session.user.id;
accept(null, true);
}
}
});
} else {
accept('INVALID SERVER: server host ' + data.address.address + ' not allowed');
}
} else {
accept('PHPSESSID is undefined', false);
}
} else {
accept('No cookie', false);
}
} else {
accept('NO ADDRESS TRANSMITTED.', false);
return false;
}
}).on('connection', function (socket) {
// bind events
eventManager.server.bind(socket);
});
componentManager.initCompleted();
I was able to resolve it myself.. :) I missed to look in the cached assets which had old code pointing to localhost and when I cleared the cached from yii assets folder, it worked.
Configuration required just one change to remove the binding of host from server.listen method which I had already mentioned in the question so that it can accept requests from any IP address on Google Compute Engine.

Node.js node-gcloud synchronous call

I'm using node-gcloud https://github.com/GoogleCloudPlatform/gcloud-node to interact with Google Cloud Storage.
I'm developing a node.js server (my first node.js project) to provide a small set of APIs to clients. Basically when an user uploads a file the API call return the signed url to show that file.
The getSignedUrl function is asynchronous https://googlecloudplatform.github.io/gcloud-node/#/docs/v0.8.1/storage?method=getSignedUrl and I can't find a way to return that result from another function.
I've started playing with Bluebird promises but I can't get to the point of it. Here is my code:
var _signedUrl = function(bucket,url,options) {
new Promise(function (resolve, reject) {
var signed_url
bucket.getSignedUrl(options, function(err, url) {
signed_url = err || url;
console.log("This is defined: " + signed_url)
return signed_url
})
})
}
var _getSignedUrl = function(url) {
new Promise(function(resolve) {
var options = config.gs
, expires = Math.round(Date.now() / 1000) + (60 * 60 * 24 * 14)
, bucket = project.storage.bucket({bucketName: config.gs.bucket, credentials: config.gs })
, signed_url = null
options.action = 'read'
options.expires = expires// 2 weeks.
options.resource= url
signed_url = resolve(_signedUrl(bucket,url,options))
console.log("This is undefined: " + signed_url)
return JSON.stringify( {url: signed_url, expires: expires} );
});
}
I think that I'm missing the basics of how it is supposed to work, so any hint will be appreciated.
Edit:
I have reworked my solution as for the first comment:
getSignedUrl: function() {
var options = config.gs
, expires = Math.round(Date.now() / 1000) + (60 * 60 * 24 * 14)
, bucket = project.storage.bucket({bucketName: config.gs.bucket, credentials: config.gs })
, signed_url = null
options.action = 'read'
options.expires = expires// 2 weeks.
options.resource= this.url
Promise.promisifyAll(bucket);
return bucket.getSignedUrlAsync(options).catch(function(err) {
return url; // ignore errors and use the url instead
}).then(function(signed_url) {
return JSON.stringify( {url: signed_url, expires: expires} );
});
}
It's not clear to me how the double return is supposed to work, but if I keep the
return bucket
what I get is this output:
{ url:
{ _bitField: 0,
_fulfillmentHandler0: undefined,
_rejectionHandler0: undefined,
_promise0: undefined,
_receiver0: undefined,
_settledValue: undefined,
_boundTo: undefined }
}
, and if remove it and keep the
return JSON.stringify( {url: signed_url, expires: expires} );
I get undefined as before. What am I missing?
Some points:
Inside a new Promise(function(res, rej){ … }) resolver callback, you actually need to call resolve() or reject() (asynchronously), not return anything.
resolve doesn't return anything. You seemed to use it like a "wait" operation that returns the result of the promise, but such is impossible. A promise is still asynchronous.
Actually, you should never need to call new Promise at all. Use Promisification instead.
Your code should rather look like
var gcloud = require('gcloud');
Promise.promisifyAll(gcloud); // if that doesn't work, call it once on a
// specific bucket instead
function getSignedUrl(url) {
var options = config.gs,
expires = Math.round(Date.now() / 1000) + (60 * 60 * 24 * 14),
bucket = project.storage.bucket({bucketName: config.gs.bucket, credentials: config.gs });
options.action = 'read';
options.expires = expires; // 2 weeks.
options.resource = url;
return bucket.getSignedUrlAsync(options).catch(function(err) {
return url; // ignore errors and use the url instead
}).then(function(signed_url) {
console.log("This is now defined: " + signed_url);
return JSON.stringify( {url: signed_url, expires: expires} );
});
}

WebRTC Video sharing app doesn't work

I have been trying to share a video stream between two clients over WebRTC for about a week now and I have no idea how to proceed any further. I'm frustrated and could really use some help from the more experienced. Please help me get this running.
I am using Websockets and NodeJS. I will post all of my code below:
Server Code ( on NodeJS )
"use strict";
/** Requires **/
var webSocketServer = require('websocket').server,
expr = require("express"),
xpress = expr(),
server = require('http').createServer(xpress);
// Configure express
xpress.configure(function() {
xpress.use(expr.static(__dirname + "/public"));
xpress.set("view options", {layout: false});
});
// Handle GET requests to root directory
xpress.get('/', function(req, res) {
res.sendfile(__dirname + '/public/index.html');
});
// WebSocket Server
var wsServer = new webSocketServer({
httpServer: server
});
// Set up the http server
server.listen(8000, function(err) {
if(!err) { console.log("Listening on port 8000"); }
});
var clients = [ ];
/** On connection established */
wsServer.on('request', function(request) {
// Accept connection - you should check 'request.origin' to make sure that client is connecting from your website
var connection = request.accept(null, request.origin);
var self = this;
// We need to know client index to remove them on 'close' event
var index = clients.push(connection) - 1;
// Event Listener for when Clients send a message to the Server
connection.on('message', function(message) {
var parsedMessage = JSON.parse(message.utf8Data);
if ( parsedMessage.kind == 'senderDescription' ) {
wsServer.broadcastUTF(JSON.stringify({ kind:'callersDescription', callerData: parsedMessage }));
}
});
});
Index.html loads and immediately runs VideoChatApp.js
function VideoChatApp() {
this.connection = null;
this.runConnection();
}
_p = VideoChatApp.prototype;
/** Initialize the connection and sets up the event listeners **/
_p.runConnection = function(){
// To allow event listeners to have access to the correct scope
var self = this;
// if user is running mozilla then use it's built-in WebSocket
window.WebSocket = window.WebSocket || window.MozWebSocket;
// if browser doesn't support WebSocket, just show some notification and exit
if (!window.WebSocket) { return; }
/** Where to make the connection **/
var host = location.origin.replace(/^http/, 'ws');
console.log(host);
this.connection = new WebSocket(host);
/** Once the connection is established **/
this.connection.onopen = function () {
console.log("Web Socket Connection Established");
self.onConnectionEstablished();
};
/** If there was a problem with the connection */
this.connection.onerror = function (error) {
console.log("ERROR with the connection *sadface*");
};
}; // end runConnection
_p.onConnectionEstablished = function() {
// My connection to the nodejs server
var websocketConnection = this.connection;
// Some local variables for use later
var mediaConstraints = {
optional: [],
mandatory: {
OfferToReceiveVideo: true
}
};
var offerer, answerer;
this.theLocalStream = null;
var amITheCaller = false;
var localVideoTag = document.getElementById('localVideoTag');
var remoteVideoTag = document.getElementById('remoteVideoTag');
window.RTCPeerConnection = window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
window.RTCSessionDescription = window.mozRTCSessionDescription || window.RTCSessionDescription;
window.RTCIceCandidate = window.mozRTCIceCandidate || window.RTCIceCandidate;
navigator.getUserMedia = navigator.mozGetUserMedia || navigator.webkitGetUserMedia;
window.URL = window.webkitURL || window.URL;
window.iceServers = {
iceServers: [{
url: 'stun:23.21.150.121'
}]
};
var callButton = document.getElementById("callButton");
callButton.onclick = callClicked;
function callClicked() {
amITheCaller = true;
setUpOffer();
}
offerer = new RTCPeerConnection(window.iceServers);
answerer = new RTCPeerConnection(window.iceServers);
/** Start Here - Set up my local stream **/
getUserMedia(function (stream) {
hookUpLocalStream(stream);
});
function getUserMedia(callback) {
navigator.getUserMedia({
video: true
}, callback, onerror);
function onerror(e) {
console.error(e);
}
}
function hookUpLocalStream(localStream) {
this.theLocalStream = localStream;
callButton.disabled = false;
localVideoTag.src = URL.createObjectURL(localStream);
localVideoTag.play();
};
/* When you click call, then we come here. Here I want to set up the offer and send it. */
function setUpOffer() {
var stream = theLocalStream;
offerer.addStream(stream);
offerer.onaddstream = function (event) {
console.log("onaddstream callback was called");
};
offerer.onicecandidate = function (event) {
if (!event || !event.candidate) return;
answerer.addIceCandidate(event.candidate);
};
offerer.createOffer(function (offer) {
offerer.setLocalDescription(offer);
console.log("------------------- What I am sending: -------------------------");
console.log(offer);
console.log(stream);
console.log("-----------------------------------------------------------------\n");
var jsonMsg = JSON.stringify( {kind:'senderDescription', streamInfo: offer, theStream: stream} );
websocketConnection.send( jsonMsg );
//answererPeer(offer, stream);
}, onSdpError, mediaConstraints);
}
/* Respond to a call */
function answererPeer(offer, stream) {
answerer.addStream(stream);
answerer.onaddstream = function (event) {
remoteVideoTag.src = URL.createObjectURL(event.stream);
remoteVideoTag.play();
};
answerer.onicecandidate = function (event) {
if (!event || !event.candidate) return;
offerer.addIceCandidate(event.candidate);
};
answerer.setRemoteDescription(offer, onSdpSucces, onSdpError);
answerer.createAnswer(function (answer) {
answerer.setLocalDescription(answer);
offerer.setRemoteDescription(answer, onSdpSucces, onSdpError);
}, onSdpError, mediaConstraints);
}
function onSdpError(e) {
console.error('onSdpError', e);
}
function onSdpSucces() {
console.log('onSdpSucces');
}
websocketConnection.onmessage = function (messageFromServer) {
console.log(" ------------------------ Message from server: -------------------- ");
var parsedMessage = JSON.parse(messageFromServer.data);
if(parsedMessage.callerData.kind = "senderDescription") {
console.log("Received a senderDescription");
console.log(parsedMessage.callerData.streamInfo);
console.log(parsedMessage.callerData.theStream);
console.log("-------------------------------------------------------------------\n");
answererPeer(parsedMessage.callerData.streamInfo, parsedMessage.callerData.theStream);
}
};
};// end onConnectionEstablished()
Finally, here are my errors:
I am not sure if this is still interesting for you, but I have some very good experience with WebRTC using PeerJS as a wrapper around it. It takes care of all the stuff you don't want to do (http://peerjs.com/). There is the client library as well as a very nice signaling server for nodejs (https://github.com/peers/peerjs-server). You can easy extend this server in your own node server.
This may not explain why your approach failed, but gets WebRTC running easily.
You can start with code that is already working and completely open. Check out easyrtc.com we have a client api, signalling server and working code. And if you have problems with that code ask us for help on Google Groups for easyrtc.

sending response in message passing on history search

I have been trying to send search history in the background page and send response back to front page. I am using message parsing .
I am unable to send response back after searching history.
--------background.js-------------
var SECONDS_PER_WEEK = 1000 * 60 * 60 * 24 * 7;
chrome.extension.onMessage.addListener(
function(request, sender, sendResponse) {
if(request === "getHistory"){
var value = getHistory();
console.log("Response" , value);
sendResponse(value);
}
}
);
function getHistory(){
var current_time = new Date().getTime();
var timeToFetch = current_time - SECONDS_PER_WEEK;
return chrome.history.search({
'text' : '',
'startTime' : timeToFetch
},
function(resp){
return resp;
});
}
The problem lies here itself as on logging I get 'undefined' as response.
Can anyone direct me to the solution
Most of the Chrome.* API functions are asynchronous so try doing it something like this:
background.js
var SECONDS_PER_WEEK = 1000 * 60 * 60 * 24 * 7;
chrome.extension.onMessage.addListener(function(request, sender, sendResponse) {
if(request === "getHistory"){
getHistory(sendResponse);
return true;
}
});
function getHistory(sendResponse){
var current_time = new Date().getTime();
var timeToFetch = current_time - SECONDS_PER_WEEK;
chrome.history.search({
'text' : '',
'startTime' : timeToFetch
},
function(resp){
console.log("Response: " + resp);
sendResponse(resp);
});
}

Resources