From 477184d72de35553d8c00a4dc8e047911481b2ef Mon Sep 17 00:00:00 2001 From: Pagan Gazzard Date: Thu, 20 Apr 2017 20:59:45 -0700 Subject: [PATCH] Add handling for duplicate UUIDs and key exchanging for old user-api-keys Change-Type: minor --- package.json | 2 +- src/bootstrap.coffee | 73 ++++++++++++++++++++++++++++++++++++++++---- src/device.coffee | 10 +++--- 3 files changed, 73 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 475dbf78..e3a46b42 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "dependencies": { "JSONStream": "^1.1.2", "blinking": "~0.0.2", - "bluebird": "^3.0.0", + "bluebird": "^3.5.0", "body-parser": "^1.12.0", "buffer-equal-constant-time": "^1.0.1", "docker-delta": "1.0.1", diff --git a/src/bootstrap.coffee b/src/bootstrap.coffee index 549abee8..c14a8640 100644 --- a/src/bootstrap.coffee +++ b/src/bootstrap.coffee @@ -2,14 +2,19 @@ Promise = require 'bluebird' knex = require './db' utils = require './utils' deviceRegister = require 'resin-register-device' +{ resinApi, request } = require './request' fs = Promise.promisifyAll(require('fs')) config = require './config' configPath = '/boot/config.json' appsPath = '/boot/apps.json' _ = require 'lodash' deviceConfig = require './device-config' +TypedError = require 'typed-error' userConfig = {} +DuplicateUuidError = message: '"uuid" must be unique.' +exports.ExchangeKeyError = class ExchangeKeyError extends TypedError + bootstrapper = {} loadPreloadedApps = -> @@ -30,6 +35,27 @@ loadPreloadedApps = -> .catch (err) -> utils.mixpanelTrack('Loading preloaded apps failed', { error: err }) +exchangeKey = -> + resinApi.get + resource: 'device' + options: + filter: + uuid: userConfig.uuid + customOptions: + apikey: userConfig.apiKey + .catchReturn([]) + .timeout(config.apiTimeout) + .then ([ device ]) -> + if not device? + throw new ExchangeKeyError("Couldn't fetch device with provisioning key") + # We found the device, we can try to generate a working device key for it + request.postAsync("#{config.apiEndpoint}/api-key/device/#{device.id}/device-key") + .spread (res, body) -> + if res.status != 200 + throw new ExchangeKeyError("Couldn't generate device key with provisioning key") + userConfig.deviceApiKey = body + .return(device) + bootstrap = -> Promise.try -> userConfig.deviceType ?= 'raspberry-pi' @@ -46,6 +72,29 @@ bootstrap = -> apiEndpoint: config.apiEndpoint ) .timeout(config.apiTimeout) + .catch DuplicateUuidError, -> + console.log('UUID already registered, checking if our device key is valid for it') + resinApi.get + resource: 'device' + options: + filter: + uuid: userConfig.uuid + customOptions: + apikey: userConfig.deviceApiKey + .catchReturn([]) + .timeout(config.apiTimeout) + .then ([ device ]) -> + if device? + console.log('Fetched device, all is good') + return device + # If we couldn't fetch with the device key then we can try to key exchange in case the provisioning key is an old 'user-api-key' + console.log("Couldn't fetch the device, trying to exchange for a valid key") + exchangeKey() + .tapCatch ExchangeKeyError, (err) -> + # If it fails we just have to reregister as a provisioning key doesn't have the ability to change existing devices + console.log('Exchanging key failed, having to reregister') + generateRegistration(true) + .then (device) -> .then ({ id }) -> userConfig.registered_at = Date.now() userConfig.deviceId = id @@ -71,18 +120,21 @@ readConfig = -> fs.readFileAsync(configPath, 'utf8') .then(JSON.parse) -readConfigAndEnsureUUID = -> +generateRegistration = (forceReregister = false) -> Promise.try -> - return userConfig.uuid if userConfig.uuid? and userConfig.deviceApiKey? - userConfig.uuid ?= deviceRegister.generateUniqueKey() - userConfig.deviceApiKey ?= deviceRegister.generateUniqueKey() + if forceReregister + userConfig.uuid = deviceRegister.generateUniqueKey() + userConfig.deviceApiKey = deviceRegister.generateUniqueKey() + else + userConfig.uuid ?= deviceRegister.generateUniqueKey() + userConfig.deviceApiKey ?= deviceRegister.generateUniqueKey() fs.writeFileAsync(configPath, JSON.stringify(userConfig)) .return(userConfig.uuid) .catch (err) -> console.log('Error generating and saving UUID: ', err) Promise.delay(config.bootstrapRetryDelay) .then -> - readConfigAndEnsureUUID() + generateRegistration() bootstrapOrRetry = -> utils.mixpanelTrack('Device bootstrap') @@ -96,6 +148,15 @@ bootstrapper.done = new Promise (resolve) -> bootstrapper.doneBootstrapping = -> bootstrapper.bootstrapped = true resolve(userConfig) + # If we're still using an old api key we can try to exchange it for a valid device key + if userConfig.apiKey? + exchangeKey() + .then -> + delete userConfig.apiKey + knex('config').update(value: userConfig.deviceApiKey).where(key: 'apiKey') + .then -> + fs.writeFileAsync(configPath, JSON.stringify(userConfig)) + bootstrapper.bootstrapped = false bootstrapper.startBootstrapping = -> @@ -111,7 +172,7 @@ bootstrapper.startBootstrapping = -> return uuid.value console.log('New device detected. Bootstrapping..') - readConfigAndEnsureUUID() + generateRegistration() .tap -> loadPreloadedApps() .tap (uuid) -> diff --git a/src/device.coffee b/src/device.coffee index 16fc2d65..98ee1b00 100644 --- a/src/device.coffee +++ b/src/device.coffee @@ -21,10 +21,10 @@ exports.getID = memoizePromise -> bootstrap.done .then -> Promise.all([ - knex('config').select('value').where(key: 'apiKey') - knex('config').select('value').where(key: 'uuid') + utils.getConfig('apiKey') + utils.getConfig('uuid') ]) - .spread ([{ value: apiKey }], [{ value: uuid }]) -> + .spread (apiKey, uuid) -> resinApi.get( resource: 'device' options: @@ -178,9 +178,9 @@ do -> if _.size(stateDiff) is 0 return applyPromise = Promise.join( - knex('config').select('value').where(key: 'apiKey') + utils.getConfig('apiKey') device.getID() - ([{ value: apiKey }], deviceID) -> + (apiKey, deviceID) -> stateDiff = getStateDiff() if _.size(stateDiff) is 0 || !apiKey? return