diff --git a/lib/db/api.js b/lib/db/api.js index a09f62d6..d1606897 100644 --- a/lib/db/api.js +++ b/lib/db/api.js @@ -1,10 +1,19 @@ var r = require('rethinkdb') +var util = require('util') var db = require('./') var wireutil = require('../wire/util') var dbapi = Object.create(null) +dbapi.DuplicateSecondaryIndexError = function DuplicateSecondaryIndexError() { + Error.call(this) + this.name = 'DuplicateSecondaryIndexError' + Error.captureStackTrace(this, DuplicateSecondaryIndexError) +} + +util.inherits(dbapi.DuplicateSecondaryIndexError, Error) + dbapi.close = function(options) { return db.close(options) } @@ -48,6 +57,49 @@ dbapi.resetUserSettings = function(email) { })) } +dbapi.insertUserAdbKey = function(email, key) { + return db.run(r.table('users').get(email).update({ + adbKeys: r.row('adbKeys').default([]).append({ + title: key.title + , fingerprint: key.fingerprint + }) + })) +} + +dbapi.deleteUserAdbKey = function(email, fingerprint) { + return db.run(r.table('users').get(email).update({ + adbKeys: r.row('adbKeys').default([]).filter(function(key) { + return key('fingerprint').ne(fingerprint) + }) + })) +} + +dbapi.lookupUsersByAdbKey = function(fingerprint) { + return db.run(r.table('users').getAll(fingerprint, { + index: 'adbKeys' + })) +} + +dbapi.lookupUserByAdbFingerprint = function(fingerprint) { + return db.run(r.table('users').getAll(fingerprint, { + index: 'adbKeys' + }) + .pluck('email', 'name', 'group')) + .then(function(cursor) { + return cursor.toArray() + }) + .then(function(groups) { + switch (groups.length) { + case 1: + return groups[0] + case 0: + return null + default: + throw new Error('Found multiple users for same ADB fingerprint') + } + }) +} + dbapi.addUserForward = function(email, forward) { var devicePort = forward.devicePort return db.run(r.table('users').get(email).update({ diff --git a/lib/db/setup.js b/lib/db/setup.js index 374ba60c..d1f6e091 100644 --- a/lib/db/setup.js +++ b/lib/db/setup.js @@ -52,8 +52,20 @@ module.exports = function(conn) { }) } - function createIndex(table, index, fn) { - return r.table(table).indexCreate(index, fn).run(conn) + function createIndex(table, index, options) { + var args = [index] + , rTable = r.table(table) + + if (options) { + if (options.indexFunction) { + args.push(options.indexFunction) + } + if (options.options) { + args.push(options.options) + } + } + + return rTable.indexCreate.apply(rTable, args).run(conn) .then(function() { log.info('Index "%s"."%s" created', table, index) }) @@ -63,7 +75,7 @@ module.exports = function(conn) { }) .catch(noMasterAvailableError, function() { return Promise.delay(1000).then(function() { - return createIndex(table, index, fn) + return createIndex(table, index, options) }) }) } diff --git a/lib/db/tables.js b/lib/db/tables.js index 2e7d1ae7..b3950a85 100644 --- a/lib/db/tables.js +++ b/lib/db/tables.js @@ -3,20 +3,34 @@ var r = require('rethinkdb') module.exports = { users: { primaryKey: 'email' + , indexes: { + adbKeys: { + indexFunction: function(user) { + return user('adbKeys')('fingerprint') + } + , options: { + multi: true + } + } + } } , devices: { primaryKey: 'serial' , indexes: { - owner: function(device) { - return r.branch( - device('present') - , device('owner')('email') - , r.literal() - ) + owner: { + indexFunction: function(device) { + return r.branch( + device('present') + , device('owner')('email') + , r.literal() + ) + } } , lastHeartbeatAt: null - , providerChannel: function(device) { - return device('provider')('channel') + , providerChannel: { + indexFunction: function(device) { + return device('provider')('channel') + } } } } diff --git a/lib/units/app/index.js b/lib/units/app/index.js index 50d13a91..67f58368 100644 --- a/lib/units/app/index.js +++ b/lib/units/app/index.js @@ -10,11 +10,13 @@ var csrf = require('csurf') var Promise = require('bluebird') var httpProxy = require('http-proxy') var compression = require('compression') +var adbkit = require('adbkit') var logger = require('../../util/logger') var pathutil = require('../../util/pathutil') var dbapi = require('../../db/api') var datautil = require('../../util/datautil') +var requtil = require('../../util/requtil') var auth = require('./middleware/auth') var deviceIconMiddleware = require('./middleware/device-icons') @@ -147,6 +149,78 @@ module.exports = function(options) { }) }) + app.post('/api/v1/app/user/keys/adb', function(req, res) { + requtil.validate(req, function() { + req.checkBody('title').notEmpty().len(1, 100) + req.checkBody('key').notEmpty() + }) + .then(function() { + return adbkit.util.parsePublicKey(req.body.key) + }) + .then(function(key) { + return dbapi.lookupUsersByAdbKey(key.fingerprint) + .then(function(users) { + if (users.length) { + throw new dbapi.DuplicateSecondaryIndexError() + } + else { + // Well, this doesn't guarantee that no one else inserts the + // same key before we do, but it's hardly a big enough problem + // to consider right now. + return dbapi.insertUserAdbKey(req.user.email, { + title: req.body.title + , fingerprint: key.fingerprint + }) + .then(function() { + res.send({ + success: true + , key: { + title: req.body.title + , fingerprint: key.fingerprint + } + }) + }) + } + }) + }) + .catch(requtil.ValidationError, function(err) { + res.status(400).send({ + success: false + , error: 'ValidationError' + , validationErrors: err.errors + }) + }) + .catch(dbapi.DuplicateSecondaryIndexError, function() { + res.status(400).send({ + success: false + , error: 'DuplicateKeyError' + }) + }) + .catch(function(err) { + log.error('Failed to insert ADB key: ', err.stack) + res.status(500).send({ + success: false + , error: 'ServerError' + }) + }) + }) + + app.delete('/api/v1/app/user/keys/adb/:id', function(req, res) { + dbapi.deleteUserAdbKey(req.user.email, req.params.id) + .then(function() { + res.send({ + success: true + }) + }) + .catch(function(err) { + log.error('Failed to delete ADB key: ', err.stack) + res.status(500).send({ + success: false + , error: 'ServerError' + }) + }) + }) + app.get('/api/v1/app/group', function(req, res) { dbapi.loadGroup(req.user.email) .then(function(cursor) { diff --git a/lib/units/device/plugins/connect.js b/lib/units/device/plugins/connect.js index e8f96352..0984bbb6 100644 --- a/lib/units/device/plugins/connect.js +++ b/lib/units/device/plugins/connect.js @@ -13,7 +13,8 @@ module.exports = syrup.serial() .dependency(require('../support/router')) .dependency(require('../support/push')) .dependency(require('./group')) - .define(function(options, adb, router, push, group) { + .dependency(require('./solo')) + .define(function(options, adb, router, push, group, solo) { var log = logger.createLogger('device:plugins:connect') , plugin = Object.create(null) , activeServer = null @@ -27,7 +28,53 @@ module.exports = syrup.serial() return resolve(plugin.url) } - var server = adb.createTcpUsbBridge(options.serial) + var server = adb.createTcpUsbBridge(options.serial, { + auth: function(key) { + var resolver = Promise.defer() + + function notify() { + push.send([ + solo.channel + , wireutil.envelope(new wire.JoinGroupByAdbFingerprintMessage( + options.serial + , key.fingerprint + , key.comment + )) + ]) + } + + function joinListener(group, identifier) { + if (identifier !== key.fingerprint) { + resolver.reject(new Error('Somebody else took the device')) + } + } + + function autojoinListener(identifier, joined) { + if (identifier === key.fingerprint) { + if (joined) { + resolver.resolve() + } + else { + resolver.reject(new Error('Device is already in use')) + } + } + } + + group.on('join', joinListener) + group.on('autojoin', autojoinListener) + router.on(wire.AdbKeysUpdatedMessage, notify) + + notify() + + return resolver.promise + .timeout(120000) + .finally(function() { + group.removeListener('join', joinListener) + group.removeListener('autojoin', autojoinListener) + router.removeListener(wire.AdbKeysUpdatedMessage, notify) + }) + } + }) server.on('listening', function() { resolve(plugin.url) diff --git a/lib/units/device/plugins/group.js b/lib/units/device/plugins/group.js index 01cc747c..6936704e 100644 --- a/lib/units/device/plugins/group.js +++ b/lib/units/device/plugins/group.js @@ -30,7 +30,7 @@ module.exports = syrup.serial() return currentGroup }) - plugin.join = function(newGroup, timeout) { + plugin.join = function(newGroup, timeout, identifier) { return plugin.get() .then(function() { if (currentGroup.group !== newGroup.group) { @@ -60,7 +60,7 @@ module.exports = syrup.serial() )) ]) - plugin.emit('join', currentGroup) + plugin.emit('join', currentGroup, identifier) return currentGroup }) @@ -134,6 +134,15 @@ module.exports = syrup.serial() ]) }) }) + .on(wire.AutoGroupMessage, function(channel, message) { + return plugin.join(message.owner, message.timeout, message.identifier) + .then(function() { + plugin.emit('autojoin', message.identifier, true) + }) + .catch(grouputil.AlreadyGroupedError, function() { + plugin.emit('autojoin', message.identifier, false) + }) + }) .on(wire.UngroupMessage, function(channel, message) { var reply = wireutil.reply(options.serial) grouputil.match(ident, message.requirements) diff --git a/lib/units/processor/index.js b/lib/units/processor/index.js index 5a39443b..0ace153a 100644 --- a/lib/units/processor/index.js +++ b/lib/units/processor/index.js @@ -70,6 +70,35 @@ module.exports = function(options) { }) }) // Worker messages + .on(wire.JoinGroupByAdbFingerprintMessage, function(channel, message) { + dbapi.lookupUserByAdbFingerprint(message.fingerprint) + .then(function(user) { + if (user) { + devDealer.send([ + channel + , wireutil.envelope(new wire.AutoGroupMessage( + new wire.OwnerMessage( + user.email + , user.name + , user.group + ) + , message.fingerprint + )) + ]) + } + else { + /* ask user */ + log.debug('ask user') + } + }) + .catch(function(err) { + log.error( + 'Unable to lookup user by fingerprint "%s"' + , message.fingerprint + , err.stack + ) + }) + }) .on(wire.JoinGroupMessage, function(channel, message, data) { dbapi.setDeviceOwner(message.serial, message.owner) appDealer.send([channel, data]) diff --git a/lib/wire/wire.proto b/lib/wire/wire.proto index 718aba70..f7424a40 100644 --- a/lib/wire/wire.proto +++ b/lib/wire/wire.proto @@ -15,6 +15,9 @@ enum MessageType { InstallMessage = 30; PhysicalIdentifyMessage = 29; JoinGroupMessage = 11; + JoinGroupByAdbFingerprintMessage = 69; + AutoGroupMessage = 70; + AdbKeysUpdatedMessage = 71; KeyDownMessage = 12; KeyPressMessage = 13; KeyUpMessage = 14; @@ -230,6 +233,11 @@ message GroupMessage { repeated DeviceRequirement requirements = 3; } +message AutoGroupMessage { + required OwnerMessage owner = 1; + required string identifier = 2; +} + message UngroupMessage { repeated DeviceRequirement requirements = 2; } @@ -239,6 +247,15 @@ message JoinGroupMessage { required OwnerMessage owner = 2; } +message JoinGroupByAdbFingerprintMessage { + required string serial = 1; + required string fingerprint = 2; + optional string comment = 3; +} + +message AdbKeysUpdatedMessage { +} + message LeaveGroupMessage { required string serial = 1; required OwnerMessage owner = 2; diff --git a/res/app/components/stf/keys/add-adb-key/add-adb-key-directive.js b/res/app/components/stf/keys/add-adb-key/add-adb-key-directive.js index d6a71003..7b0ac3ce 100644 --- a/res/app/components/stf/keys/add-adb-key/add-adb-key-directive.js +++ b/res/app/components/stf/keys/add-adb-key/add-adb-key-directive.js @@ -7,26 +7,29 @@ module.exports = function addAdbKeyDirective(AdbKeysService) { showClipboard: '=' }, template: require('./add-adb-key.jade'), - link: function (scope) { - scope.addForm = { - title: '', - key: '' + controller: function($scope, UserService) { + $scope.addForm = { + title: '' + , key: '' } - scope.addKey = function () { - console.log('Add key') - scope.closeAddKey() + $scope.addKey = function () { + UserService.addAdbKey({ + title: $scope.addForm.title + , key: $scope.addForm.key + }) + $scope.closeAddKey() } - scope.closeAddKey = function () { - scope.addForm.title = '' - scope.addForm.key = '' - console.log('scope', scope) + $scope.closeAddKey = function () { + $scope.addForm.title = '' + $scope.addForm.key = '' // TODO: cannot access to the form by name inside a directive? - //scope.adbkeyform.$setPristine() - scope.showAdd = false + //$scope.adbkeyform.$setPristine() + $scope.showAdd = false } - + }, + link: function (scope) { scope.$watch('addForm.key', function (newValue) { if (newValue && !scope.addForm.title) { // By default sets the title to the ADB key comment because @@ -34,7 +37,6 @@ module.exports = function addAdbKeyDirective(AdbKeysService) { scope.addForm.title = AdbKeysService.commentFromKey(newValue) } }) - } } } diff --git a/res/app/components/stf/user/user-service.js b/res/app/components/stf/user/user-service.js index 5c66d7c5..4357c13a 100644 --- a/res/app/components/stf/user/user-service.js +++ b/res/app/components/stf/user/user-service.js @@ -1,5 +1,27 @@ -module.exports = function UserServiceFactory(AppState) { - var userService = {} - userService.currentUser = AppState.user - return userService +module.exports = function UserServiceFactory($http, AppState) { + var UserService = {} + + var user = UserService.currentUser = AppState.user + + UserService.getAdbKeys = function() { + return (user.adbKeys || (user.adbKeys = [])) + } + + UserService.addAdbKey = function(key) { + return $http.post('/api/v1/app/user/keys/adb', key) + .success(function(data) { + UserService.getAdbKeys().push(data.key) + }) + } + + UserService.removeAdbKey = function(key) { + return $http.delete('/api/v1/app/user/keys/adb/' + key.fingerprint) + .success(function() { + user.adbKeys = UserService.getAdbKeys().filter(function(someKey) { + return someKey.fingerprint !== key.fingerprint + }) + }) + } + + return UserService } diff --git a/res/app/settings/keys/adb-keys/adb-keys-controller.js b/res/app/settings/keys/adb-keys/adb-keys-controller.js index 8645ad84..9e830b0b 100644 --- a/res/app/settings/keys/adb-keys/adb-keys-controller.js +++ b/res/app/settings/keys/adb-keys/adb-keys-controller.js @@ -1,24 +1,19 @@ -module.exports = function AdbKeysCtrl($scope, AddAdbKeyModalService) { +module.exports = function AdbKeysCtrl($scope, $http, UserService, AddAdbKeyModalService) { //AddAdbKeyModalService.open({ // title: 'PC1264', // fingerprint: 'bb:86:60:39:d7:a2:e3:09:93:09:cc:f6:e8:37:99:3f' //}) - $scope.adbKeys = [ - { - title: 'A11251@PC1264.local', - fingerprint: 'bb:86:60:39:d7:a2:e3:09:93:09:cc:f6:e8:37:99:3f' - }, - { - title: 'A11251@MobileMac.local', - fingerprint: '97:ca:ae:fa:09:0b:c4:fe:22:94:7d:b2:be:77:66:a1' - } - ] + $scope.adbKeys = [] - $scope.removeKey = function (key) { - console.log('Remove key', key) - $scope.adbKeys.splice($scope.adbKeys.indexOf(key), 1) + function updateKeys() { + $scope.adbKeys = UserService.getAdbKeys() } + $scope.removeKey = function (key) { + UserService.removeAdbKey(key).then(updateKeys) + } + + updateKeys() } diff --git a/res/app/settings/keys/adb-keys/adb-keys-service.js b/res/app/settings/keys/adb-keys/adb-keys-service.js index 045009d3..03526a4f 100644 --- a/res/app/settings/keys/adb-keys/adb-keys-service.js +++ b/res/app/settings/keys/adb-keys/adb-keys-service.js @@ -3,7 +3,7 @@ module.exports = function AdbKeysServiceFactory() { service.hostNameFromKey = function (key) { if (key.match(/.+= (.+)/)) { - return key.replace(/.+= (.+)/g, '$1').replace(/(\.local)?/g, '') + return key.replace(/.+= (.+)/g, '$1') } return '' }