How to debug Directory API push notifications? - javascript

I used Directory API push notifications example of https://stackoverflow.com/users/6586255/dimu-designs from this question: Is it possible to watch Directory API changes from Google App Maker/Google Apps Script?
I have two projects setup, another one is webhook itself:
/** HTTP GET request handler */
function doGet(e) {
return ContentService.createTextOutput("GET message");
}
/** HTTP POST request handler */
function doPost(e) {
return ContentService.createTextOutput("POST message");
}
And other one is the watch request:
function startUpdateWatch() {
var channel = AdminDirectory.newChannel(),
receivingURL = "https://script.google.com/macros/s/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/exec",
gSuiteDomain = "[business-name].com",
event = "update";
channel.id = Utilities.getUuid();
channel.type = "web_hook";
channel.address = receivingURL + "?domain=" + gSuiteDomain + "&event=" + event;
channel.expiration = Date.now() + 21600000; // max of 6 hours in the future; Note: watch must be renew before expiration to keep sending notifications
AdminDirectory.Users.watch(
channel,
{
"domain":gSuiteDomain,
"event":event
}
);
}
Webhook url works fine, it fires "Get" when I try to open it via browser. startUpdateWatch function starts fine as well without errors and if I try to change the url to something else, I get an error, so that receiving url should be ok as well.
How you debug that push channel? I have tried to launch function with different events like 'add' and 'delete' as well and then deleted/added/modified user in G suite, but no push from that function towards the url.

Related

How to get access token from node-webkit for a desktop app without hosting page?

I'm trying to create a desktop app with node-webkit. A part of this app require the use of facebook to get/post some content to any profile (facebook user) that use my desktop app on my personal computer.
Regarding facebook api documentation (https://developers.facebook.com/docs/facebook-login/manually-build-a-login-flow/v2.0) , I have to manually implement the login flow and use the following URI as the redirect URI: https://www.facebook.com/connect/login_success.html
Currently, in my node-webkit app, via a child window (a popup), a user can login to facebook and authorize my desktop app to interact with it's profile.
Here is a part of the code:
var url = "https://www.facebook.com/dialog/oauth?client_id=myclientID&redirect_uri=https://www.facebook.com/connect/login_success.html&response_type=token&scope=publish_actions";
loginWindow = window.open(url, 'Login facebook', 'location=yes,toolbar=yes,menubar=yes,directories=yes,status=yes,resizable=yes,scrollbars=yes,height=480,width=640', false);
After that, the user is redirected to the following URI and as mentioned in the doc, the access token appear correctly:
https://www.facebook.com/connect/login_success.html#access_token=theBigTokenString&expires_in=3864.
But it appears only for few seconds and after that the url is replaced by https://www.facebook.com/connect/blank.html#_=_ (with a security warning message).
I've read some post that propose to add eventListener like hashchange to the opened window in order to capture the access token. But after some redirect within the child window, I'm no longer available to interact with it via javascript.
So finally, I can't get the access token that the child window has retrieved and make visible for few seconds.
Is anyone can help me to get this user access token with node-webkit?
I really don't want to use a server (for hosting a web page) between my desktop app and facebook.
Thanks in advance for help.
With the help of an other question (here) I found a solution to my problem.
I post the code that I use now and I Hope it will help someone else:
var url = "https://www.facebook.com/dialog/oauth?&client_id=myBigClientID&redirect_uri=https://www.facebook.com/connect/login_success.html&response_type=token&scope=publish_actions";
function Response() {
this.access_token = null;
this.expires_in = null;
};
var response = new Response();
//function called every 500ms to check if token is in url
window.hashUpdate = function() {
if(window.loginWindow.closed){
window.clearInterval(intervalId);
start(); //just a callback that I'm using to start another part of my application (after I caught the token)
}
else {
var url = window.loginWindow.document.URL;
var tabUrl = url.split("#"); //first split to separate the domain part and the parameter part
var paramString = tabUrl[1]; //I concerned only by the second part after the '#'
if(paramString != undefined){
var allParam = paramString.split("&");
for (var i = 0; i < allParam.length; i++) {
var oneParamKeyValue = allParam[i].split("=");
response[oneParamKeyValue[0]] = oneParamKeyValue[1]; //store my token in form of key => value
};
//close the window after 1500ms
setTimeout(function(){
window.loginWindow.close();
}, 1500);
}
}
}
//open the url and start the watching process
window.loginWindow = window.open(this.url, 'Login facebook', false);
this.intervalId = window.setInterval("window.hashUpdate()", 500);

Getting unique ClientID from chrome extension?

I'm developing chrome extension. I need the ability to identify each client as a unique client.
I can't store guid in a cookie since cookie can be deleted. I need something to be read from the system itself which is unique.
Now - I know that JS doesn't has access to client resources ( local resources) but - and here is my question :
Question
Does chrome extensions Js's provide API for getting unique client information ( I dont care what data - as long as it is unique).
Edit :
Just to clarify :
The user will be shown a unique key ( which is a hash data of his computer). this code will be sent to me , and I will provide matching result which the user will be sent (via email) and only then - he will be able to use the extension.
(no , not all countries support extension payment via wallet , im at one of those countries)
To uniquely identify a user, I would suggest to generate a random token and store it in your extension's storage (chrome.storage). The userid has to be generated only once, when the token does not exist in storage.
For example:
function getRandomToken() {
// E.g. 8 * 32 = 256 bits token
var randomPool = new Uint8Array(32);
crypto.getRandomValues(randomPool);
var hex = '';
for (var i = 0; i < randomPool.length; ++i) {
hex += randomPool[i].toString(16);
}
// E.g. db18458e2782b2b77e36769c569e263a53885a9944dd0a861e5064eac16f1a
return hex;
}
chrome.storage.sync.get('userid', function(items) {
var userid = items.userid;
if (userid) {
useToken(userid);
} else {
userid = getRandomToken();
chrome.storage.sync.set({userid: userid}, function() {
useToken(userid);
});
}
function useToken(userid) {
// TODO: Use user id for authentication or whatever you want.
}
});
This mechanism relies on chrome.storage.sync, which is quite reliable. This stored ID will only be lost in the following scenarios:
The user re-installs the extension. Local storage will be cleared when uninstalling the extension.
One of the storage quotas has been exceeded (read the documentation).
This is not going to happen because the only write operation occurs at the first run of your extension.
Chrome's storage gets corrupted and fails to save the data.
Even if the user does not have Chrome Sync enabled, data will still be saved locally. There have been bugs with Chrome's internals that resulted in data loss, but these are incidents.
The user has opened the developer tools for your extension page and ran chrome.storage.sync.clear() or something similar.
You cannot protect against users who possess the knowledge to mess with the internals of Chrome extensions.
The previous method is sufficient if you want to uniquely identify a user. If you really want to get a hardware-based ID, use chrome.storage.cpu and chrome.storage.memory as well. I don't see any benefits in using these additional sources though, because they can change if the user replaces hardware, and they are not unique either (two identical laptops would report the same values, for instance).
As Xan suggested, the chrome.identity API is probably your best choice. You can get the users e-mail address and use that as a random seed to generate a code of your choosing. The user info also includes an "id" field which I believe is unique but I haven't ever seen any documentation that substantiates that. You can then use the chrome.storage.sync API to store the generated key in the users online data storage for your app. This way the user will be able to access their private key whenever and where ever they log in on any device.
Please note that you will have to enable the oAuth2 api's in the developers console for your application and include the application key and proper scopes in your app manifest.
Here is a crude example:
function getUserInfo (interactive, callback )
{
var xmlhttp = new XMLHttpRequest();
var retry = true;
var access_token;
getToken();
/**
* Request the Auth Token
*/
function getToken()
{
chrome.identity.getAuthToken( { 'interactive': interactive }, function (token) {
if ( chrome.runtime.lastError )
{
console.log( "ERROR! " + chrome.runtime.lastError.message );
return;
}
if ( typeof token != 'undefined ')
{
access_token = token;
sendRequest( );
}
else
callback( );
});
}
function sendRequest()
{
xmlhttp.open('GET', 'https://www.googleapis.com/userinfo/v2/me' );
xmlhttp.setRequestHeader('Authorization','Bearer ' + access_token );
xmlhttp.onload = requestComplete;
xmlhttp.send();
}
function requestComplete()
{
if ( this.status == 401 && retry )
{
retry = false; // only retry once
console.log( "Request failed, retrying... " + this.response );
}
else
{
console.log( "Request completed. User Info: " + this.response );
callback(null, this.status, this.response );
var userInfo = JSON.parse( this.response );
storeUniqueKey( userInfo );
}
}
}
function storeUniqueKey( info )
{
var key;
// TODO: Generate some key using the user info: info.loginName
// user info here contains several fields you might find useful.
// There is a user "id" field here which is numeric and I believe that
// is a unique identifier that could come in handy rather than generating your
// own key.
...
chrome.storage.sync.set ( { user_key: key } );
}
To add to Rob W's answer. In his method, the saved string would propagate to every Chrome instance signed in with the same Google user account - with a lot of big and small if's.
If you need to uniquely identify a local user profile, and not all Chrome profiles with the same Google user, you want to employ chrome.storage.local in the same manner. This will NOT be a unique Chrome install identifier though - only a profile within that install.
What also needs to be noted is that all this data is not in any way or form tied to anything - it just has a good probability of being unique. But absolutely nothing stops user from reading and cloning this data as he sees fit. You cannot, in this scenario, secure the client side.
I'm thinking that a more secure way would be to use chrome.identity API to request and maintain an offline (therefore, not expiring) token as proof of license. The user cannot easily clone this token storage.
I'm not versed in OAuth yet, so if anyone can point out what's wrong with this idea - they are welcome to.
We can also use Crypto.randomUUID() for generating a UUID and then save it to web storage. Refer to MSDN for details this API.
let uuid = self.crypto.randomUUID();
console.log(uuid); // for example "36b8f84d-df4e-4d49-b662-bcde71a8764f"

Execute web worker from different origin

I am developing a library which I want to host on a CDN. The library is going to be used on many different domains across multiple servers. The library itself contains one script (let's call it script.js for now) which loads a web worker (worker.js).
Loading the library itself is quite easy: just add the <script type="text/javascript" src="http://cdn.mydomain.com/script.js"></script> tag to the domain on which I want to use the library (www.myotherdomain.com). However since the library is loading a worker from http://cdn.mydomain.com/worker.js new Worker('http://cdn.mydomain.com/worker.js'), I get a SecurityException. CORS is enabled on cdn.mydomain.com.
For web workers it is not allowed to use a web worker on a remote domain. Using CORS will not help: browsers seem to ignore it and don't even execute the preflight check.
A way around this would be to perform an XMLHttpRequest to get the source of the worker and then create a BLOB url and create a worker using this url. This works for Firefox and Chrome. However, this does not seem to work for Internet Explorer or Opera.
A solution would be to place the worker on www.myotherdomain.com or place a proxy file (which simply loads the worker from the cdn using XHR or importScripts). I do not however like this solution: it requires me to place additional files on the server and since the library is used on multiple servers, updating would be difficult.
My question consists of two parsts:
Is it possible to have a worker on a remote origin for IE 10+?
If 1 is the case, how is it handled best to be working cross-browser?
The best is probably to generate a simple worker-script dynamically, which will internally call importScripts(), which is not limited by this cross-origin restriction.
To understand why you can't use a cross-domain script as a Worker init-script, see this answer. Basically, the Worker context will have its own origin set to the one of that script.
// The script there simply posts back an "Hello" message
// Obviously cross-origin here
const cross_origin_script_url = "https://greggman.github.io/doodles/test/ping-worker.js";
const worker_url = getWorkerURL( cross_origin_script_url );
const worker = new Worker( worker_url );
worker.onmessage = (evt) => console.log( evt.data );
URL.revokeObjectURL( worker_url );
// Returns a blob:// URL which points
// to a javascript file which will call
// importScripts with the given URL
function getWorkerURL( url ) {
const content = `importScripts( "${ url }" );`;
return URL.createObjectURL( new Blob( [ content ], { type: "text/javascript" } ) );
}
For those who find this question:
YES.
It is absolutely possible: the trick is leveraging an iframe on the remote domain and communicating with it through postMessage. The remote iframe (hosted on cdn.mydomain.com) will be able to load the webworker (located at cdn.mydomain.com/worker.js) since they both have the same origin.
The iframe can then act as a proxy between the postMessage calls. The script.js will however be responsible from filtering the messages so only valid worker messages are handled.
The downside is that communication speeds (and data transfer speeds) do take a performance hit.
In short:
script.js appends iframe with src="//cdn.mydomain.com/iframe.html"
iframe.html on cdn.mydomain.com/iframe.html, executes new Worker("worker.js") and acts as a proxy for message events from window and worker.postMessage (and the other way around).
script.js communicates with the worker using iframe.contentWindow.postMessage and the message event from window. (with the proper checks for the correct origin and worker identification for multiple workers)
It's not possible to load a web worker from a different domain.
Similar to your suggestion, you could make a fetch call, then take that JS and base64 it. Doing so allows you to do:
const worker = new Worker(`data:text/javascript;base64,${btoa(workerJs)}`)
You can find out more info about data URIs here: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs.
This is the workaround I prefer because it doesn't require anything crazy like an iframe with a message proxy and is very simple to get working provided you setup CORS correctly from your CDN.
Since #KevinGhadyani answer (or blob techniques) require to lessen your CSPs (by adding a worker-src data: or blob: directive, for example), there is a little example of how you can take advantage of importScripts inside a worker to load another worker script hosted on another domain, without lessening your CSPs.
It may help you to load a worker from any CDN allowed by your CSPs.
As far as I know, it works on Opera, Firefox, Chrome, Edge and all browsers that support workers.
/**
* This worker allow us to import a script from our CDN as a worker
* avoiding to have to reduce security policy.
*/
/**
* Send a formated response to the main thread. Can handle regular errors.
* #param {('imported'|'error')} resp
* #param {*} data
*/
function respond(resp, data = undefined){
const msg = { resp };
if(data !== undefined){
if(data && typeof data === 'object'){
msg.data = {};
if(data instanceof Error){
msg.error = true;
msg.data.code = data.code;
msg.data.name = data.name;
msg.data.stack = data.stack.toString();
msg.data.message = data.message;
} else {
Object.assign(msg.data, data);
}
} else msg.data = data;
}
self.postMessage(msg);
}
function handleMessage(event){
if(typeof event.data === 'string' && event.data.match(/^#worker-importer/)){
const [
action = null,
data = null
] = event.data.replace('#worker-importer.','').split('|');
switch(action){
case 'import' :
if(data){
try{
importScripts(data);
respond('imported', { url : data });
//The work is done, we can just unregister the handler
//and let the imported worker do it's work without us.
self.removeEventListener('message', handleMessage);
}catch(e){
respond('error', e);
}
} else respond('error', new Error(`No url specified.`));
break;
default : respond('error', new Error(`Unknown action ${action}`));
}
}
}
self.addEventListener('message', handleMessage);
How to use it ?
Obviously, your CSPs must allow the CDN domain, but you don't need more CSP rule.
Let's say that you domain is my-domain.com, and your cdn is statics.your-cdn.com.
The worker we want to import is hosted at https://statics.your-cdn.com/super-worker.js and will contain :
self.addEventListener('message', event => {
if(event.data === 'who are you ?') {
self.postMessage("It's me ! I'm useless, but I'm alive !");
} else self.postMessage("I don't understand.");
});
Assuming that you host a file with the code of the worker importer on your domain (NOT your CDN) under the path https://my-domain.com/worker-importer.js, and that you try to start your worker inside a script tag at https://my-domain.com/, this is how it works :
<script>
window.addEventListener('load', async () => {
function importWorker(url){
return new Promise((resolve, reject) => {
//The worker importer
const workerImporter = new Worker('/worker-importer.js');
//Will only be used to import our worker
function handleImporterMessage(event){
const { resp = null, data = null } = event.data;
if(resp === 'imported') {
console.log(`Worker at ${data.url} successfully imported !`);
workerImporter.removeEventListener('message', handleImporterMessage);
// Now, we can work with our worker. It's ready !
resolve(workerImporter);
} else if(resp === 'error'){
reject(data);
}
}
workerImporter.addEventListener('message', handleImporterMessage);
workerImporter.postMessage(`#worker-importer.import|${url}`);
});
}
const worker = await importWorker("https://statics.your-cdn.com/super-worker.js");
worker.addEventListener('message', event => {
console.log('worker message : ', event.data);
});
worker.postMessage('who are you ?');
});
</script>
This will print :
Worker at https://statics.your-cdn.com/super-worker.js successfully imported !
worker message : It's me ! I'm useless, but I'm alive !
Note that the code above can even work if it's written in a file hosted on the CDN too.
This is especially usefull when you have several worker scripts on your CDN, or if you build a library that must be hosted on a CDN and you want your users to be able to call your workers without having to host all workers on their domain.

Error: The page has been destroyed and can no longer be used

I'm developing an add-on for the first time. It puts a little widget in the status bar that displays the number of unread Google Reader items. To accommodate this, the add-on process queries the Google Reader API every minute and passes the response to the widget. When I run cfx test I get this error:
Error: The page has been destroyed and can no longer be used.
I made sure to catch the widget's detach event and stop the refresh timer in response, but I'm still seeing the error. What am I doing wrong? Here's the relevant code:
// main.js - Main entry point
const tabs = require('tabs');
const widgets = require('widget');
const data = require('self').data;
const timers = require("timers");
const Request = require("request").Request;
function refreshUnreadCount() {
// Put in Google Reader API request
Request({
url: "https://www.google.com/reader/api/0/unread-count?output=json",
onComplete: function(response) {
// Ignore response if we encountered a 404 (e.g. user isn't logged in)
// or a different HTTP error.
// TODO: Can I make this work when third-party cookies are disabled?
if (response.status == 200) {
monitorWidget.postMessage(response.json);
} else {
monitorWidget.postMessage(null);
}
}
}).get();
}
var monitorWidget = widgets.Widget({
// Mandatory widget ID string
id: "greader-monitor",
// A required string description of the widget used for
// accessibility, title bars, and error reporting.
label: "GReader Monitor",
contentURL: data.url("widget.html"),
contentScriptFile: [data.url("jquery-1.7.2.min.js"), data.url("widget.js")],
onClick: function() {
// Open Google Reader when the widget is clicked.
tabs.open("https://www.google.com/reader/view/");
},
onAttach: function(worker) {
// If the widget's inner width changes, reflect that in the GUI
worker.port.on("widthReported", function(newWidth) {
worker.width = newWidth;
});
var refreshTimer = timers.setInterval(refreshUnreadCount, 60000);
// If the monitor widget is destroyed, make sure the timer gets cancelled.
worker.on("detach", function() {
timers.clearInterval(refreshTimer);
});
refreshUnreadCount();
}
});
// widget.js - Status bar widget script
// Every so often, we'll receive the updated item feed. It's our job
// to parse it.
self.on("message", function(json) {
if (json == null) {
$("span#counter").attr("class", "");
$("span#counter").text("N/A");
} else {
var newTotal = 0;
for (var item in json.unreadcounts) {
newTotal += json.unreadcounts[item].count;
}
// Since the cumulative reading list count is a separate part of the
// unread count info, we have to divide the total by 2.
newTotal /= 2;
$("span#counter").text(newTotal);
// Update style
if (newTotal > 0)
$("span#counter").attr("class", "newitems");
else
$("span#counter").attr("class", "");
}
// Reports the current width of the widget
self.port.emit("widthReported", $("div#widget").width());
});
Edit: I've uploaded the project in its entirety to this GitHub repository.
I think if you use the method monitorWidget.port.emit("widthReported", response.json); you can fire the event. It the second way to communicate with the content script and the add-on script.
Reference for the port communication
Reference for the communication with postMessage
I guess that this message comes up when you call monitorWidget.postMessage() in refreshUnreadCount(). The obvious cause for it would be: while you make sure to call refreshUnreadCount() only when the worker is still active, this function will do an asynchronous request which might take a while. So by the time this request completes the worker might be destroyed already.
One solution would be to pass the worker as a parameter to refreshUnreadCount(). It could then add its own detach listener (remove it when the request is done) and ignore the response if the worker was detached while the request was performed.
function refreshUnreadCount(worker) {
var detached = false;
function onDetach()
{
detached = true;
}
worker.on("detach", onDetach);
Request({
...
onComplete: function(response) {
worker.removeListener("detach", onDetach);
if (detached)
return; // Nothing to update with out data
...
}
}).get();
}
Then again, using try..catch to detect this situation and suppress the error would probably be simpler - but not exactly a clean solution.
I've just seen your message on irc, thanks for reporting your issues.
You are facing some internal bug in the SDK. I've opened a bug about that here.
You should definitely keep the first version of your code, where you send messages to the widget, i.e. widget.postMessage (instead of worker.postMessage). Then we will have to fix the bug I linked to in order to just make your code work!!
Then I suggest you to move the setInterval to the toplevel, otherwise you will fire multiple interval and request, one per window. This attach event is fired for each new firefox window.

Gdata JavaScript Authsub continues redirect

I am using the JavaScript Google Data API and having issues getting the AuthSub script to work correctly. This is my script currently:
google.load('gdata', '1');
function getCookie(c_name){
if(document.cookie.length>0){
c_start=document.cookie.indexOf(c_name + "=");
if(c_start!=-1){
c_start=c_start + c_name.length+1;
c_end=document.cookie.indexOf(";",c_start);
if(c_end==-1) c_end=document.cookie.length;
return unescape(document.cookie.substring(c_start, c_end));
}
}
return "";
}
function main(){
var scope = 'http://www.google.com/calendar/feeds/';
if(!google.accounts.user.checkLogin(scope)){
google.accounts.user.login();
} else {
/*
* Retrieve all calendars
*/
// Create the calendar service object
var calendarService = new google.gdata.calendar.CalendarService('GoogleInc-jsguide-1.0');
// The default "allcalendars" feed is used to retrieve a list of all
// calendars (primary, secondary and subscribed) of the logged-in user
var feedUri = 'http://www.google.com/calendar/feeds/default/allcalendars/full';
// The callback method that will be called when getAllCalendarsFeed() returns feed data
var callback = function(result) {
// Obtain the array of CalendarEntry
var entries = result.feed.entry;
//for (var i = 0; i < entries.length; i++) {
var calendarEntry = entries[0];
var calendarTitle = calendarEntry.getTitle().getText();
alert('Calendar title = ' + calendarTitle);
//}
}
// Error handler to be invoked when getAllCalendarsFeed() produces an error
var handleError = function(error) {
alert(error);
}
// Submit the request using the calendar service object
calendarService.getAllCalendarsFeed(feedUri, callback, handleError);
}
}
google.setOnLoadCallback(main);
However when I run this the page redirects me to the authentication page. After I authenticate it send me back to my page and then quickly sends me back to the authenticate page again. I've included alerts to check if the token is being set and it doesn't seem to be working. Has anyone has this problem?
I was having the same problem so I built this function
function login() {
var scope = "http://www.google.com/calendar/feeds/";
if(!google.accounts.user.checkLogin(scope)) {
if(google.accounts.user.getStatus() == 0) {
var token = google.accounts.user.login();
}
}
}
I added the check to google.accounts.user.getStatus() if it's 1 that means the application is in the process of logging in and if it is 2 that means the applications is logged in. You can also pass a scope to the getStatus method.
the problem is that setting the cookie takes a little while when google redirects back to your site. However, the callback runs immediately, and there is no cookie by that time to verify authentication, so it again redirects back to google. Try using setTimeout or something to run the authentication check after a second or so to be sure.
You should pass the scope to the login method too.
Sometimes you can end up with an orphaned cookie in your browser - which will keep getting fed back to Google.
What I'm doing now, is doing a checkLogin before I perform my login call, and if it returns true I explicitly call logOut().
The logOut call will remove any cookies which Google had rejected but left in your browser. The reason it seems to keep going in a loop is because the cookie is there, but even on reauth, it doesn't produce a new one because you already have one. But unfortunately for our sake, the one that's there is invalid.

Categories

Resources