diff --git a/package.json b/package.json index ddd08d81..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", @@ -37,7 +37,7 @@ "pubnub": "^3.7.13", "request": "^2.51.0", "request-progress": "^2.0.1", - "resin-register-device": "^2.0.0", + "resin-register-device": "^3.0.0", "rimraf": "^2.5.4", "rwlock": "^5.0.0", "sqlite3": "^3.1.0", diff --git a/src/app.coffee b/src/app.coffee index 05da7010..847d813d 100644 --- a/src/app.coffee +++ b/src/app.coffee @@ -51,17 +51,17 @@ knex.init.then -> ) ) + updateIpAddr = -> + utils.gosuper.getAsync('/v1/ipaddr', { json: true }) + .spread (response, body) -> + if response.statusCode == 200 && body.Data.IPAddresses? + device.updateState( + ip_address: body.Data.IPAddresses.join(' ') + ) + .catch(_.noop) + console.log('Starting periodic check for IP addresses..') + setInterval(updateIpAddr, 30 * 1000) # Every 30s + updateIpAddr() + console.log('Starting Apps..') application.initialize() - - updateIpAddr = -> - utils.gosuper.getAsync('/v1/ipaddr', { json: true }) - .spread (response, body) -> - if response.statusCode == 200 && body.Data.IPAddresses? - device.updateState( - ip_address: body.Data.IPAddresses.join(' ') - ) - .catch(_.noop) - console.log('Starting periodic check for IP addresses..') - setInterval(updateIpAddr, 30 * 1000) # Every 30s - updateIpAddr() diff --git a/src/bootstrap.coffee b/src/bootstrap.coffee index e9a451a1..c14a8640 100644 --- a/src/bootstrap.coffee +++ b/src/bootstrap.coffee @@ -2,17 +2,18 @@ Promise = require 'bluebird' knex = require './db' utils = require './utils' deviceRegister = require 'resin-register-device' -{ resinApi } = require './request' +{ 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 = (err) -> - return err.message == '"uuid" must be unique.' +DuplicateUuidError = message: '"uuid" must be unique.' +exports.ExchangeKeyError = class ExchangeKeyError extends TypedError bootstrapper = {} @@ -34,27 +35,71 @@ 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' if userConfig.registered_at? return userConfig - deviceRegister.register(resinApi, userConfig) + + deviceRegister.register( + userId: userConfig.userId + applicationId: userConfig.applicationId + uuid: userConfig.uuid + deviceType: userConfig.deviceType + deviceApiKey: userConfig.deviceApiKey + provisioningApiKey: userConfig.apiKey + 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.apiKey + apikey: userConfig.deviceApiKey + .catchReturn([]) .timeout(config.apiTimeout) .then ([ device ]) -> - return device - .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 = device.id + userConfig.deviceId = id + # Delete the provisioning key now. + delete userConfig.apiKey fs.writeFileAsync(configPath, JSON.stringify(userConfig)) .return(userConfig) .then (userConfig) -> @@ -63,7 +108,7 @@ bootstrap = -> .then -> knex('config').insert([ { key: 'uuid', value: userConfig.uuid } - { key: 'apiKey', value: userConfig.apiKey } + { key: 'apiKey', value: userConfig.deviceApiKey } { key: 'username', value: userConfig.username } { key: 'userId', value: userConfig.userId } { key: 'version', value: utils.supervisorVersion } @@ -75,19 +120,21 @@ readConfig = -> fs.readFileAsync(configPath, 'utf8') .then(JSON.parse) -readConfigAndEnsureUUID = -> +generateRegistration = (forceReregister = false) -> Promise.try -> - return userConfig.uuid if userConfig.uuid? - deviceRegister.generateUUID() - .then (uuid) -> - userConfig.uuid = uuid - fs.writeFileAsync(configPath, JSON.stringify(userConfig)) - .return(uuid) + 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') @@ -101,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 = -> @@ -115,13 +171,16 @@ bootstrapper.startBootstrapping = -> bootstrapper.doneBootstrapping() if !bootstrapper.offlineMode return uuid.value console.log('New device detected. Bootstrapping..') - readConfigAndEnsureUUID() + + generateRegistration() .tap -> loadPreloadedApps() .tap (uuid) -> if bootstrapper.offlineMode return knex('config').insert({ key: 'uuid', value: uuid }) else - return bootstrapOrRetry() + bootstrapOrRetry() + # Don't wait on bootstrapping here, bootstrapper.done is for that. + return module.exports = bootstrapper 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 diff --git a/src/proxyvisor.coffee b/src/proxyvisor.coffee index 72b3fb7c..111fed5c 100644 --- a/src/proxyvisor.coffee +++ b/src/proxyvisor.coffee @@ -50,9 +50,9 @@ router.post '/v1/devices', (req, res) -> utils.getConfig('apiKey') utils.getConfig('userId') device.getID() - deviceRegister.generateUUID() randomHexString.generate() - (apiKey, userId, deviceId, uuid, logsChannel) -> + (apiKey, userId, deviceId, logsChannel) -> + uuid = deviceRegister.generateUniqueKey() d = user: userId application: req.body.appId diff --git a/tools/dind/vpn-init b/tools/dind/vpn-init index 394bcd0c..5452807b 100755 --- a/tools/dind/vpn-init +++ b/tools/dind/vpn-init @@ -7,7 +7,7 @@ while true; do echo "UUID missing from config file, VPN cannot connect" sleep 2 else - read uuid api_key <<<$(jq -r '.uuid,.apiKey' $CONFIG_PATH) + read uuid api_key <<<$(jq -r '.uuid,.deviceApiKey // .apiKey' $CONFIG_PATH) mkdir -p /var/volatile/ echo $uuid > /var/volatile/vpnfile echo $api_key >> /var/volatile/vpnfile