From 44d0f6b255982cd8a2d887b5adb5fdd277609702 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6lsche?= Date: Sun, 27 Jan 2013 01:26:24 +0100 Subject: [PATCH] translated to coffee, made API more verbose, added phantomJS script to automate web-based authorization (WIP) --- .gitignore | 1 + index.coffee | 160 ++++++++++++++ index.js | 256 ++++++++++++++--------- lib/google-login-phantomjs-script.coffee | 95 +++++++++ package.json | 11 +- test/test.coffee | 41 ++++ test/test.js | 90 -------- 7 files changed, 461 insertions(+), 193 deletions(-) create mode 100644 index.coffee create mode 100644 lib/google-login-phantomjs-script.coffee create mode 100644 test/test.coffee delete mode 100644 test/test.js diff --git a/.gitignore b/.gitignore index a4eb816..a6bc8dc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ config +node_modules lib-cov *.seed *.log diff --git a/index.coffee b/index.coffee new file mode 100644 index 0000000..2164fb7 --- /dev/null +++ b/index.coffee @@ -0,0 +1,160 @@ +request = require("request") +exec = require("child_process").exec +querystring = require("querystring") + +endpoint_auth = "https://accounts.google.com/o/oauth2/auth" +endpoint_token = "https://accounts.google.com/o/oauth2/token" + +module.exports = (opts) -> + opts.redirect_uri or= "http://localhost:3000/callback" + opts.refresh_tokens or= [] + + ### + Constructs a google OAuth2 request url using the provided opts. + Spawns an http server to handle the redirect. Once user authenticates + and the server parses the auth code, the server process is closed + (Assuming the user has closed the window. For future, add redirect after + authentication to a page with instructions to close tab/window). + + Some scopes: + https://www.googleapis.com/auth/userinfo.profile + https://www.googleapis.com/auth/drive.readonly.metadata + ### + + getAuthCode = (scope, openURICallback, callback) -> + + # default: open the system's web browser + openURICallback or= (uri, cb) -> + #TODO: Make OS agnostic w/ xdg-open, open, etc. + exec "open '#{uri}'", cb + + qs = + response_type: "code" + client_id: opts.client_id + redirect_uri: opts.redirect_uri + scope: scope + + uri = endpoint_auth + "?" + querystring.stringify(qs) + + parseCode = (req, res) -> + #for gdrive-cli: + #instead of just closing the connection, redirect to an informational page + console.log "Stopping server ..." + server.close() + + #split url by ? so we just have the querystring left + #extract out the auth code + callback null, querystring.parse(req.url.split("?")[1]).code + + console.log "Starting server ..." + server = require("http").createServer((req, res) -> + parseCode req, res if req.url.match(/callback/) + ).listen(3000) + + openURICallback uri, (err) -> + callback err + + + ### + Given the acquired authorization code and the provided opts, + construct a POST request to acquire the access token and refresh + token. + + @param {String} code Can be acquired with getAuthCode + ### + getTokensForAuthCode = (code, callback) -> + form = + code: code + client_id: opts.client_id + client_secret: opts.client_secret + redirect_uri: opts.redirect_uri + grant_type: "authorization_code" + + request.post + url: endpoint_token + form: form + , (err, req, body) -> + if err? then return callback err + callback null, JSON.parse(body) + + ### + Given a refresh token and provided opts, returns a new + access token. Tyically the access token is valid for an hour. + + @param {String} refresh_token The refresh token. Can be acquired + through getTokensForAuthCode function. + ### + getAccessTokenForRefreshToken = (refresh_token, callback) -> + form = + refresh_token: refresh_token + client_id: opts.client_id + client_secret: opts.client_secret + grant_type: "refresh_token" + + request.post + url: endpoint_token + form: form + , (err, req, body) -> + if err? then callback err + callback null, JSON.parse(body) + + + ### + Given google account name and password, use phantomJS to login a user into google services. + Afterwards navigate the browser to a given URL. + The purpose of this is to allow a command line tool to authorize + an application (or itself) to access the user's data. + ### + + automaticGoogleWebLogin = (username, password, followUpURI, cb) -> + childProcess = require "child_process" + phantomjs = require "phantomjs" + path = require "path" + + childArgs = [ + path.join(__dirname, "lib/google-login-phantomjs-script.coffee") + username + password + followUpURI + ] + + child = childProcess.spawn phantomjs.path, childArgs + + child.stdout.on "data", (data) -> + process.stdout.write data + + child.stderr.on "data", (data) -> + process.stderr.write data + + child.on "exit", (code) -> + console.log "phantomjs exited with code:", code + if code isnt 0 + return cb "phantomjs exited with code #{code}", code + cb null + + + authorizeApplication = (username, password, scope, cb) -> + getAuthCode scope, (uri, cb) -> + automaticGoogleWebLogin username, password, uri, cb + , cb + + ### + Convenience dunction + Use this to get an access token for a specific scope + ### + getAccessToken: (scope, cb) -> + refresh_token = opts.refresh_tokens[scope] + if refresh_token + getAccessTokenForRefreshToken refresh_token, cb + else + async.waterfall [ + getAuthCode, + getTokensForAuthCode + ], (err, result) -> + # store the refresh_token for future use + opts.refresh_token[scope] = result?.refresh_token + cb err, result?.access_token + + return { + authorizeApplication: authorizeApplication + } diff --git a/index.js b/index.js index 49d727b..0740fd6 100644 --- a/index.js +++ b/index.js @@ -1,112 +1,168 @@ -var request = require('request'), - exec = require('child_process').exec, - querystring = require('querystring'); +// Generated by CoffeeScript 1.3.3 +(function() { + var endpoint_auth, endpoint_token, exec, querystring, request; + request = require("request"); -module.exports = function(opts) { - if(!opts.redirect_uri){ - opts.redirect_uri = 'http://localhost:3000/callback'; - } + exec = require("child_process").exec; - var gAuth = {}; + querystring = require("querystring"); - var endpoint_auth = 'https://accounts.google.com/o/oauth2/auth'; - var endpoint_token = 'https://accounts.google.com/o/oauth2/token'; - /** - * Constructs a google OAuth2 request url using the provided opts. - * Spawns an http server to handle the redirect. Once user authenticates - * and the server parses the auth code, the server process is closed - * (Assuming the user has closed the window. For future, add redirect after - * authentication to a page with instructions to close tab/window). - */ - function getAuthCode(callback){ - var qs = { - response_type: 'code', - client_id: opts.client_id, - redirect_uri: opts.redirect_uri, - //scope: 'https://www.googleapis.com/auth/userinfo.profile' - scope: "https://www.googleapis.com/auth/drive.readonly.metadata" - }; + endpoint_auth = "https://accounts.google.com/o/oauth2/auth"; - var uri = '\''+endpoint_auth + '?' + querystring.stringify(qs) +'\''; - - //TODO: Make OS agnostic w/ xdg-open, open, etc. - exec('open '+uri, function(err){ - if(err !== null){ - callback(err); - } - var server = require('http').createServer(function(req, res) { - if(req.url.match(/callback/)) return parseCode(req, res); - }).listen(3000); + endpoint_token = "https://accounts.google.com/o/oauth2/token"; - function parseCode(req,res){ - //for gdrive-cli: - //instead of just closing the connection, redirect to an informational page + module.exports = function(opts) { + var authorizeApplication, automaticGoogleWebLogin, getAccessTokenForRefreshToken, getAuthCode, getTokensForAuthCode; + opts.redirect_uri || (opts.redirect_uri = "http://localhost:3000/callback"); + opts.refresh_tokens || (opts.refresh_tokens = []); + /* + Constructs a google OAuth2 request url using the provided opts. + Spawns an http server to handle the redirect. Once user authenticates + and the server parses the auth code, the server process is closed + (Assuming the user has closed the window. For future, add redirect after + authentication to a page with instructions to close tab/window). + + Some scopes: + https://www.googleapis.com/auth/userinfo.profile + https://www.googleapis.com/auth/drive.readonly.metadata + */ + + getAuthCode = function(scope, openURICallback, callback) { + var parseCode, qs, server, uri; + openURICallback || (openURICallback = function(uri, cb) { + return exec("open '" + uri + "'", cb); + }); + qs = { + response_type: "code", + client_id: opts.client_id, + redirect_uri: opts.redirect_uri, + scope: scope + }; + uri = endpoint_auth + "?" + querystring.stringify(qs); + parseCode = function(req, res) { + console.log("Stopping server ..."); server.close(); - - //split url by ? so we just have the querystring left - //extract out the auth code - callback(null, querystring.parse(req.url.split('?')[1]).code); - } - - }); - } - - /** - * Given the acquired authorization code and the provided opts, - * construct a POST request to acquire the access token and refresh - * token. - * - * @param {String} code The acquired authorization code - */ - function getToken(code, callback){ - var form = { - code : code, - client_id : opts.client_id, - client_secret: opts.client_secret, - redirect_uri: opts.redirect_uri, - grant_type: 'authorization_code' + return callback(null, querystring.parse(req.url.split("?")[1]).code); + }; + console.log("Starting server ..."); + server = require("http").createServer(function(req, res) { + if (req.url.match(/callback/)) { + return parseCode(req, res); + } + }).listen(3000); + return openURICallback(uri, function(err) { + return callback(err); + }); }; - - request.post({url:endpoint_token, form: form}, function(err,req, body){ - if(err !== null){ - callback(err); - }else{ - callback(null, JSON.parse(body)); - } - }); - } - - /** - * Given a refresh token and provided opts, returns a new - * access. - * - * @param {String} refresh_token The refresh token. Can be acquired - * through getToken function. - */ - function refreshToken(refresh_token, callback){ - var form = { - refresh_token: refresh_token, - client_id: opts.client_id, - client_secret: opts.client_secret, - grant_type:'refresh_token' + /* + Given the acquired authorization code and the provided opts, + construct a POST request to acquire the access token and refresh + token. + + @param {String} code Can be acquired with getAuthCode + */ + + getTokensForAuthCode = function(code, callback) { + var form; + form = { + code: code, + client_id: opts.client_id, + client_secret: opts.client_secret, + redirect_uri: opts.redirect_uri, + grant_type: "authorization_code" + }; + return request.post({ + url: endpoint_token, + form: form + }, function(err, req, body) { + if (err != null) { + return callback(err); + } + return callback(null, JSON.parse(body)); + }); }; - - request.post({url: endpoint_token, form:form}, function(err, req, body){ - if(err !== null){ - callback(err); - }else{ - callback(null, JSON.parse(body)); + /* + Given a refresh token and provided opts, returns a new + access token. Tyically the access token is valid for an hour. + + @param {String} refresh_token The refresh token. Can be acquired + through getTokensForAuthCode function. + */ + + getAccessTokenForRefreshToken = function(refresh_token, callback) { + var form; + form = { + refresh_token: refresh_token, + client_id: opts.client_id, + client_secret: opts.client_secret, + grant_type: "refresh_token" + }; + return request.post({ + url: endpoint_token, + form: form + }, function(err, req, body) { + if (err != null) { + callback(err); + } + return callback(null, JSON.parse(body)); + }); + }; + /* + Given google account name and password, use phantomJS to login a user into google services. + Afterwards navigate the browser to a given URL. + The purpose of this is to allow a command line tool to authorize + an application (or itself) to access the user's data. + */ + + automaticGoogleWebLogin = function(username, password, followUpURI, cb) { + var child, childArgs, childProcess, path, phantomjs; + childProcess = require("child_process"); + phantomjs = require("phantomjs"); + path = require("path"); + childArgs = [path.join(__dirname, "lib/google-login-phantomjs-script.coffee"), username, password, followUpURI]; + child = childProcess.spawn(phantomjs.path, childArgs); + child.stdout.on("data", function(data) { + return process.stdout.write(data); + }); + child.stderr.on("data", function(data) { + return process.stderr.write(data); + }); + return child.on("exit", function(code) { + console.log("phantomjs exited with code:", code); + if (code !== 0) { + return cb("phantomjs exited with code " + code, code); + } + return cb(null); + }); + }; + authorizeApplication = function(username, password, scope, cb) { + return getAuthCode(scope, function(uri, cb) { + return automaticGoogleWebLogin(username, password, uri, cb); + }, cb); + }; + ({ + /* + Convenience dunction + Use this to get an access token for a specific scope + */ + + getAccessToken: function(scope, cb) { + var refresh_token; + refresh_token = opts.refresh_tokens[scope]; + if (refresh_token) { + return getAccessTokenForRefreshToken(refresh_token, cb); + } else { + return async.waterfall([getAuthCode, getTokensForAuthCode], function(err, result) { + opts.refresh_token[scope] = result != null ? result.refresh_token : void 0; + return cb(err, result != null ? result.access_token : void 0); + }); + } } }); - } - - gAuth.getAuthCode = getAuthCode; - - gAuth.getToken = getToken; - - gAuth.refreshToken = refreshToken; - - return gAuth; + return { + authorizeApplication: authorizeApplication + }; + }; -}; +}).call(this); diff --git a/lib/google-login-phantomjs-script.coffee b/lib/google-login-phantomjs-script.coffee new file mode 100644 index 0000000..63ec6bf --- /dev/null +++ b/lib/google-login-phantomjs-script.coffee @@ -0,0 +1,95 @@ +page = require('webpage').create() +system = require('system') + +debug = true +jqueryURI = "http://ajax.googleapis.com/ajax/libs/jquery/1.6.1/jquery.min.js" + +if debug + page.onConsoleMessage = (msg) -> console.log(msg) + page.onAlert = (msg) -> console.log(msg) + + #page.onResourceRequested = (request) -> + # console.log 'Request ' + JSON.stringify request, undefined, 4 + # console.log request.url + #page.onResourceReceived = (response) -> + # console.log 'Receive ' + JSON.stringify response, undefined, 4 + +page.onUrlChanged = (url) -> + #console.log "new url: #{url}" + if /settings\/account/.test url + console.log "logged in #{system.args[1]}" + followUpURI = system.args[3] + # do on next tick (to prevent from crashing when exit is called from an event handler) + setTimeout -> + confirmPermissions followUpURI + , 1 + +page.open "https://accounts.google.com/ServiceLogin", (status) -> + if status is "success" + page.includeJs jqueryURI, -> + page.evaluate (username, password) -> + #alert "* Script running in the Page context." + $("input").each -> + console.log "input", $(this).attr("type"), this.id, this.name, $(this).val() + $("input[type=email]").val(username) + $("input[type=password]").val(password) + $("input[type=submit]").click() + , system.args[1], system.args[2] + + setTimeout -> + page.render "timeoui.png" + console.log("TIMEOUT! See timeout.png.") + phantom.exit 1 + , 15000 + + else + console.log "... fail! Check the $PWD?!" + phantom.exit 1 + +confirmPermissions = (uri) -> + console.log "navigating to: #{uri}" + + confirmPage = require('webpage').create() + confirmPage.onConsoleMessage = (msg) -> console.log(msg) + #confirmPage.onAlert = (msg) -> console.log(msg) + confirmPage.onUrlChanged = (url) -> + console.log "new confirm url #{url}" + + confirmPage.open uri, (status) -> + console.log status + if status isnt "success" + console.log "page failed to load, see fail.png" + confirmPage.render "fail.png" + phantom.exit 1 + + pos = null + + confirmPage.includeJs jqueryURI, -> + pos = confirmPage.evaluate -> + $("button").each -> + console.log "button", $(this).attr("type"), this.id, this.name, $(this).val() + + button = $("button").first() + {left, top} = button.offset() + [width, height] = [button.width(), button.height()] + + button.click() + + return { + x: Math.round left + width/2 + y: Math.round top + height/2 + } + + setTimeout -> + console.log "approving by clicking at", pos.x, pos.y + confirmPage.sendEvent 'mousedown', pos.x, pos.y, "left" + confirmPage.sendEvent 'mouseup', pos.x, pos.y, "left" + confirmPage.sendEvent 'click', pos.x, pos.y + , 2000 + + + setTimeout -> + confirmPage.render "timeoui.png" + console.log("TIMEOUT while approving! See timeout.png.") + phantom.exit 1 + , 8000 diff --git a/package.json b/package.json index 5459560..f97193b 100644 --- a/package.json +++ b/package.json @@ -20,13 +20,18 @@ }, "main": "lib/gdrive.js", "dependencies": { - "request": ">= 2.12.0" + "request": ">= 2.12.0", + "async": "~0.1.22" }, "devDependencies": { "mocha": ">= 1.7.4", - "chai": "~1.4.2" + "chai": "~1.4.2", + "coffee-script": "~1.4.0" }, "scripts": { - "test": "mocha test/*" + "test": "./node_modules/mocha/bin/mocha --timeout 20000 --compilers coffee:coffee-script test/*" + }, + "optionalDependencies": { + "phantomjs": "~1.8.0-1" } } diff --git a/test/test.coffee b/test/test.coffee new file mode 100644 index 0000000..f1505cd --- /dev/null +++ b/test/test.coffee @@ -0,0 +1,41 @@ +should = require("chai").should() +request = require("request") +config = require("../config/config.js") +gAuth = require("../index.js")(config) +{inspect} = require 'util' + +refresh_token = undefined +access_token = undefined + +describe "Google Oauth", -> + auth_code = undefined + describe "#getAuthCode()", -> + it "Responds with authorization code", (done) -> + gAuth.authorizeApplication config.username, config.password, config.scope,(err, code) -> + should.not.exist err + should.exist code + code.should.be.a "string" + auth_code = code + done() + + # describe "#getTokensForAuthCode()", -> + # it "Respond with access token adn a refresh token", (done) -> + # gAuth.getTokensForAuthCode auth_code, (err, body) -> + # should.not.exist err + # should.exist body + # body.should.be.an "object" + # + # should.exist body.access_token + # should.exist body.refresh_token + # + # done() + # + # describe "#getAccessTokenForRefreshToken()", -> + # it "Respond with new access token, expiration time", (done) -> + # gAuth.getAccessTokenForRefreshToken refresh_token, (err, body) -> + # should.not.exist err + # should.exist body + # body.should.be.an "object" + # console.log inspect body + # should.exist body.access_token + # done() diff --git a/test/test.js b/test/test.js deleted file mode 100644 index ef9cf25..0000000 --- a/test/test.js +++ /dev/null @@ -1,90 +0,0 @@ -var should = require('chai').should(); -var request = require("request"); -var config = require('../config/config.js'); - -var gAuth = require('../index.js')(config); - -var refresh_token = config.refresh_token; -var access_token; - -describe('Google Oauth', function(){ - - var auth_code; - - if (!refresh_token) { - describe('#getAuthCode()', function(){ - it('Responds with authorization code', function(done){ - gAuth.getAuthCode(function(err, code){ - should.not.exist(err); - should.exist(code); - code.should.be.a('string'); - auth_code = code; - done(); - }); - }); - }); - - describe('#getToken()', function(){ - it('Respond with access token, refresh token, etc', function(done){ - gAuth.getToken(auth_code, function(err, body){ - should.not.exist(err); - should.exist(body); - body.should.be.an('object'); - //body.should.have.length(5); - access_token = body.access_token; - refresh_token = body.refresh_token; - console.log("STORE this refresh_token: " + refresh_token); - - //Add value tests - done(); - }); - }); - }); - } - - describe('#refreshToken()', function(){ - it('Respond with new access token, expiration time, etc', function(done){ - gAuth.refreshToken(refresh_token, function(err, body){ - should.not.exist(err); - should.exist(body); - body.should.be.an('object'); - //body.should.have.length(4); - access_token = body.access_token; - console.log("access_token: " + access_token); - var total = 0; - var getChunk = function(link) { - console.log(link); - request.get({ - url: link, - headers: { - "Authorization": "Bearer " + access_token, - "Content-Type": "application/json" - } - }, myCallback); - } - var myCallback = function(err, res, body){ - if (err) { - console.log("err:" + err); - return done(); - } - //console.log(body); - var o = JSON.parse(body); - total += o.items.length; - console.log(o.items.length + " items"); - var nextLink = o.nextLink; - if (nextLink) { - getChunk(nextLink); - } else { - console.log("total: " + total + " items.") - done(); - } - }; - var link = "https://www.googleapis.com/drive/v2/files"; - getChunk(link); - }); - }); - }); -}); - - -