mirror of
https://github.com/balena-os/balena-supervisor.git
synced 2025-03-21 19:45:19 +00:00
Auto-merge for PR #547 via VersionBot
Implement an API for proxy and hostname configuration, and centralize…
This commit is contained in:
commit
7e04b4aec0
@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file
|
||||
automatically by Versionist. DO NOT EDIT THIS FILE MANUALLY!
|
||||
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## v6.6.0 - 2018-01-18
|
||||
|
||||
* Implement an API for proxy and hostname configuration, and centralize management of config.json #547 [Pablo Carranza Velez]
|
||||
|
||||
## v6.5.9 - 2018-01-12
|
||||
|
||||
* Fix saving deviceApiKey to the DB (to fix the RESIN_API_KEY env var) when updating from some older supervisors #544 [Pablo Carranza Velez]
|
||||
|
127
docs/API.md
127
docs/API.md
@ -14,7 +14,7 @@ The supervisor exposes an HTTP API on port 48484 (`RESIN_SUPERVISOR_PORT`).
|
||||
|
||||
The full address for the API, i.e. `"http://127.0.0.1:48484"`, is available as `RESIN_SUPERVISOR_ADDRESS`. **Always use these variables when communicating via the API, since address and port could change**.
|
||||
|
||||
Alternatively, the Resin API (api.resin.io) has a proxy endpoint at `POST /supervisor/<url>` (where `<url>` is one of the API URLs described below) from which you can send API commands to the supervisor remotely, using your Auth Token instead of your API key. Commands sent through the proxy require an `appId` and/or `deviceId` parameter in the body, and default to POST requests unless you specify a `method` parameter (e.g. "GET").
|
||||
Alternatively, the Resin API (api.resin.io) has a proxy endpoint at `POST /supervisor/<url>` (where `<url>` is one of the API URLs described below) from which you can send API commands to the supervisor remotely, using your Auth Token instead of your API key. Commands sent through the proxy can specify either an `appId` to send the request to all devices in an application, or a `deviceId` or `uuid` to send to a particular device. These requests default to POST unless you specify a `method` parameter (e.g. "GET"). In the examples below, we show how to use a uuid to specify a device, but in any of those you can replace `uuid` for a `deviceId` or `appId`.
|
||||
|
||||
The API is versioned (currently at v1), except for `/ping`.
|
||||
|
||||
@ -43,7 +43,7 @@ Remotely via the API proxy:
|
||||
```bash
|
||||
$ curl -X POST --header "Content-Type:application/json" \
|
||||
--header "Authorization: Bearer <auth token>" \
|
||||
--data '{"deviceId": <deviceId>, "appId": <appId>, "method": "GET"}' \
|
||||
--data '{"uuid": <uuid>, "method": "GET"}' \
|
||||
"https://api.resin.io/supervisor/ping"
|
||||
```
|
||||
|
||||
@ -67,7 +67,7 @@ Remotely via the API proxy:
|
||||
```bash
|
||||
$ curl -X POST --header "Content-Type:application/json" \
|
||||
--header "Authorization: Bearer <auth token>" \
|
||||
--data '{"deviceId": <deviceId>, "appId": <appId>}' \
|
||||
--data '{"uuid": <uuid>}' \
|
||||
"https://api.resin.io/supervisor/v1/blink"
|
||||
```
|
||||
|
||||
@ -100,7 +100,7 @@ Remotely via the API proxy:
|
||||
```bash
|
||||
$ curl -X POST --header "Content-Type:application/json" \
|
||||
--header "Authorization: Bearer <auth token>" \
|
||||
--data '{"deviceId": <deviceId>, "appId": <appId>, "data": {"force": true}}' \
|
||||
--data '{"uuid": <uuid>, "data": {"force": true}}' \
|
||||
"https://api.resin.io/supervisor/v1/update"
|
||||
```
|
||||
|
||||
@ -134,7 +134,7 @@ Remotely via the API proxy:
|
||||
```bash
|
||||
$ curl -X POST --header "Content-Type:application/json" \
|
||||
--header "Authorization: Bearer <auth token>" \
|
||||
--data '{"deviceId": <deviceId>, "appId": <appId>}' \
|
||||
--data '{"uuid": <uuid>}' \
|
||||
"https://api.resin.io/supervisor/v1/reboot"
|
||||
```
|
||||
|
||||
@ -169,7 +169,7 @@ Remotely via the API proxy:
|
||||
```bash
|
||||
$ curl -X POST --header "Content-Type:application/json" \
|
||||
--header "Authorization: Bearer <auth token>" \
|
||||
--data '{"deviceId": <deviceId>, "appId": <appId>}' \
|
||||
--data '{"uuid": <uuid>}' \
|
||||
"https://api.resin.io/supervisor/v1/shutdown"
|
||||
```
|
||||
|
||||
@ -217,7 +217,7 @@ Remotely via the API proxy:
|
||||
```bash
|
||||
$ curl -X POST --header "Content-Type:application/json" \
|
||||
--header "Authorization: Bearer <auth token>" \
|
||||
--data '{"deviceId": <deviceId>, "appId": <appId>, "data": {"appId": <appId>}}' \
|
||||
--data '{"uuid": <uuid>, "data": {"appId": <appId>}}' \
|
||||
"https://api.resin.io/supervisor/v1/purge"
|
||||
```
|
||||
|
||||
@ -260,7 +260,7 @@ Remotely via the API proxy:
|
||||
```bash
|
||||
$ curl -X POST --header "Content-Type:application/json" \
|
||||
--header "Authorization: Bearer <auth token>" \
|
||||
--data '{"deviceId": <deviceId>, "appId": <appId>, "data": {"appId": <appId>}}' \
|
||||
--data '{"uuid": <uuid>, "data": {"appId": <appId>}}' \
|
||||
"https://api.resin.io/supervisor/v1/restart"
|
||||
```
|
||||
|
||||
@ -285,7 +285,7 @@ Remotely via the API proxy:
|
||||
```bash
|
||||
$ curl -X POST --header "Content-Type:application/json" \
|
||||
--header "Authorization: Bearer <auth token>" \
|
||||
--data '{"deviceId": <deviceId>, "appId": <appId>}' \
|
||||
--data '{"uuid": <uuid>}' \
|
||||
"https://api.resin.io/supervisor/v1/tcp-ping"
|
||||
```
|
||||
|
||||
@ -310,7 +310,7 @@ Remotely via the API proxy:
|
||||
```bash
|
||||
$ curl -X POST --header "Content-Type:application/json" \
|
||||
--header "Authorization: Bearer <auth token>" \
|
||||
--data '{"deviceId": <deviceId>, "appId": <appId>, "method": "DELETE"}' \
|
||||
--data '{"uuid": <uuid>, "method": "DELETE"}' \
|
||||
"https://api.resin.io/supervisor/v1/tcp-ping"
|
||||
```
|
||||
|
||||
@ -335,7 +335,7 @@ Remotely via the API proxy:
|
||||
```bash
|
||||
$ curl -X POST --header "Content-Type:application/json" \
|
||||
--header "Authorization: Bearer <auth token>" \
|
||||
--data '{"deviceId": <deviceId>, "appId": <appId>}' \
|
||||
--data '{"uuid": <uuid>}' \
|
||||
"https://api.resin.io/supervisor/v1/regenerate-api-key"
|
||||
```
|
||||
|
||||
@ -374,7 +374,7 @@ Remotely via the API proxy:
|
||||
```bash
|
||||
$ curl -X POST --header "Content-Type:application/json" \
|
||||
--header "Authorization: Bearer <auth token>" \
|
||||
--data '{"deviceId": <deviceId>, "appId": <appId>, "method": "GET"}' \
|
||||
--data '{"uuid": <uuid>, "method": "GET"}' \
|
||||
"https://api.resin.io/supervisor/v1/device"
|
||||
```
|
||||
|
||||
@ -412,7 +412,7 @@ Remotely via the API proxy:
|
||||
```bash
|
||||
$ curl -X POST --header "Content-Type:application/json" \
|
||||
--header "Authorization: Bearer <auth token>" \
|
||||
--data '{"deviceId": <deviceId>, "appId": <appId>}' \
|
||||
--data '{"uuid": <uuid>}' \
|
||||
"https://api.resin.io/supervisor/v1/apps/<appId>/stop"
|
||||
```
|
||||
|
||||
@ -446,7 +446,7 @@ Remotely via the API proxy:
|
||||
```bash
|
||||
$ curl -X POST --header "Content-Type:application/json" \
|
||||
--header "Authorization: Bearer <auth token>" \
|
||||
--data '{"deviceId": <deviceId>, "appId": <appId>}' \
|
||||
--data '{"uuid": <uuid>}' \
|
||||
"https://api.resin.io/supervisor/v1/apps/<appId>/start"
|
||||
```
|
||||
|
||||
@ -480,7 +480,7 @@ Remotely via the API proxy:
|
||||
```bash
|
||||
$ curl -X POST --header "Content-Type:application/json" \
|
||||
--header "Authorization: Bearer <auth token>" \
|
||||
--data '{"deviceId": <deviceId>, "appId": <appId>, "method": "GET"}' \
|
||||
--data '{"uuid": <uuid>, "method": "GET"}' \
|
||||
"https://api.resin.io/supervisor/v1/apps/<appId>"
|
||||
```
|
||||
|
||||
@ -507,6 +507,101 @@ Remotely via the API proxy:
|
||||
```bash
|
||||
$ curl -X POST --header "Content-Type:application/json" \
|
||||
--header "Authorization: Bearer <auth token>" \
|
||||
--data '{"deviceId": <deviceId>, "appId": <appId>, "method": "GET"}' \
|
||||
--data '{"uuid": <uuid>, "method": "GET"}' \
|
||||
"https://api.resin.io/supervisor/v1/healthy"
|
||||
```
|
||||
|
||||
<hr>
|
||||
|
||||
### PATCH /v1/device/host-config
|
||||
|
||||
Added in supervisor v6.6.0.
|
||||
|
||||
This endpoint allows setting some configuration values for the host OS. Currently it supports
|
||||
proxy and hostname configuration.
|
||||
|
||||
For proxy configuration, resinOS 2.0.7 and higher provides a transparent proxy redirector (redsocks) that makes all connections be routed to a SOCKS or HTTP proxy. This endpoint allows user applications to modify these proxy settings at runtime.
|
||||
|
||||
|
||||
#### Request body
|
||||
|
||||
Is a JSON object with several optional fields. Proxy and hostname configuration go under a "network" key. If "proxy" or "hostname" are not present (undefined), those values will not be modified, so that a request can modify hostname
|
||||
without changing proxy settings and viceversa.
|
||||
|
||||
```json
|
||||
{
|
||||
"network": {
|
||||
"proxy": {
|
||||
"type": "http-connect",
|
||||
"ip": "myproxy.example.com",
|
||||
"port": 8123,
|
||||
"login": "username",
|
||||
"password": "password",
|
||||
"noProxy": [ "152.10.30.4", "253.1.1.0/16" ]
|
||||
},
|
||||
"hostname": "mynewhostname"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
In the proxy settings, `type`, `ip`, `port`, `login` and `password` are the settings for the proxy redirector to
|
||||
be able to connnect to the proxy, based on how [redsocks.conf](https://github.com/darkk/redsocks/blob/master/redsocks.conf.example) works. `type` can be `socks4`, `socks5`, `http-connect` or `http-relay` (not all proxies are
|
||||
guaranteed to work, especially if they block connections that the resin services may require).
|
||||
|
||||
Keep in mind that, even if transparent proxy redirection will take effect immediately after the API call (i.e. all new connections will go through the proxy), open connections will not be closed. So, if for example, the device has managed to connect to the resin VPN without the proxy, it will stay connected directly without trying to reconnect through the proxy, unless the connection breaks - any reconnection attempts will then go through the proxy. To force *all* connections to go through the proxy, the best way is to reboot the device (see the /v1/reboot endpoint). In most networks were no connections to the Internet can be made if not through a proxy, this should not be necessary (as there will be no open connections before configuring the proxy settings).
|
||||
|
||||
The "noProxy" setting for the proxy is an optional array of IP addresses/subnets that should not be routed through the
|
||||
proxy. Keep in mind that local/reserved subnets are already [excluded by resinOS automatically](https://github.com/resin-os/meta-resin/blob/master/meta-resin-common/recipes-connectivity/resin-proxy-config/resin-proxy-config/resin-proxy-config#L48).
|
||||
|
||||
If either "proxy" or "hostname" are null or empty values (i.e. `{}` for proxy or an empty string for hostname), they will be cleared to their default values (i.e. not using a proxy, and a hostname equal to the first 7 characters of the device's uuid, respectively).
|
||||
|
||||
#### Examples:
|
||||
From the app on the device:
|
||||
```bash
|
||||
$ curl -X PATCH --header "Content-Type:application/json" \
|
||||
--data '{"network": {"hostname": "newhostname"}}' \
|
||||
"$RESIN_SUPERVISOR_ADDRESS/v1/device/host-config?apikey=$RESIN_SUPERVISOR_API_KEY"
|
||||
```
|
||||
|
||||
Response:
|
||||
```none
|
||||
OK
|
||||
```
|
||||
|
||||
Remotely via the API proxy:
|
||||
```bash
|
||||
$ curl -X POST --header "Content-Type:application/json" \
|
||||
--header "Authorization: Bearer <auth token>" \
|
||||
--data '{"uuid": <uuid>, "method": "PATCH", "data": {"network": {"hostname": "newhostname"}}}' \
|
||||
"https://api.resin.io/supervisor/v1/device/host-config"
|
||||
```
|
||||
|
||||
<hr>
|
||||
|
||||
### GET /v1/device/host-config
|
||||
|
||||
Added in supervisor v6.6.0.
|
||||
|
||||
This endpoint allows reading some configuration values for the host OS, previously set with `PATCH /v1/device/host-config`. Currently it supports
|
||||
proxy and hostname configuration.
|
||||
|
||||
Please refer to the PATCH endpoint above for details on the behavior and meaning of the fields in the response.
|
||||
|
||||
#### Examples:
|
||||
From the app on the device:
|
||||
```bash
|
||||
$ curl "$RESIN_SUPERVISOR_ADDRESS/v1/device/host-config?apikey=$RESIN_SUPERVISOR_API_KEY"
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{"network":{"proxy":{"ip":"192.168.0.199","port":"8123","type":"socks5"},"hostname":"27b0fdc"}}
|
||||
```
|
||||
|
||||
Remotely via the API proxy:
|
||||
```bash
|
||||
$ curl -X POST --header "Content-Type:application/json" \
|
||||
--header "Authorization: Bearer <auth token>" \
|
||||
--data '{"uuid": <uuid>, "method": "GET"}' \
|
||||
"https://api.resin.io/supervisor/v1/device/host-config"
|
||||
```
|
||||
|
@ -38,6 +38,10 @@ type LogToDisplayBody struct {
|
||||
Enable bool
|
||||
}
|
||||
|
||||
type RestartServiceBody struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func jsonResponse(writer http.ResponseWriter, response interface{}, status int) {
|
||||
jsonBody, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
@ -278,3 +282,29 @@ func LogToDisplayControl(writer http.ResponseWriter, request *http.Request) {
|
||||
sendResponse(true, "", http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
//RestartService is used to restart a systemd service by name
|
||||
func RestartService(writer http.ResponseWriter, request *http.Request) {
|
||||
sendResponse, sendError := responseSenders(writer)
|
||||
var body RestartServiceBody
|
||||
if err := parseJSONBody(&body, request); err != nil {
|
||||
log.Println(err)
|
||||
sendResponse("Error", err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if systemd.Dbus == nil {
|
||||
sendError(fmt.Errorf("Systemd dbus unavailable, cannot restart service."))
|
||||
return
|
||||
}
|
||||
|
||||
actionDescr := "Service Restart"
|
||||
|
||||
if _, err := systemd.Dbus.RestartUnit(body.Name+".service", "fail", nil); err != nil {
|
||||
log.Printf("%s: %s\n", actionDescr, err)
|
||||
sendError(err)
|
||||
return
|
||||
}
|
||||
log.Printf("%sd\n", actionDescr)
|
||||
sendResponse("OK", "", http.StatusOK)
|
||||
}
|
||||
|
@ -26,6 +26,7 @@ func setupApi(router *mux.Router) {
|
||||
apiv1.HandleFunc("/shutdown", ShutdownHandler).Methods("POST")
|
||||
apiv1.HandleFunc("/vpncontrol", VPNControl).Methods("POST")
|
||||
apiv1.HandleFunc("/set-log-to-display", LogToDisplayControl).Methods("POST")
|
||||
apiv1.HandleFunc("/restart-service", RestartService).Methods("POST")
|
||||
}
|
||||
|
||||
func startApi(listenAddress string, router *mux.Router) {
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "resin-supervisor",
|
||||
"description": "This is resin.io's Supervisor, a program that runs on IoT devices and has the task of running user Apps (which are Docker containers), and updating them as Resin's API informs it to.",
|
||||
"version": "6.5.9",
|
||||
"version": "6.6.0",
|
||||
"license": "Apache-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -40,6 +40,7 @@
|
||||
"log-timestamp": "^0.1.2",
|
||||
"memoizee": "^0.4.1",
|
||||
"mixpanel": "0.0.20",
|
||||
"mkdirp": "^0.5.1",
|
||||
"network-checker": "~0.0.5",
|
||||
"node-loader": "^0.6.0",
|
||||
"null-loader": "^0.1.1",
|
||||
|
@ -7,6 +7,7 @@ config = require './config'
|
||||
device = require './device'
|
||||
_ = require 'lodash'
|
||||
proxyvisor = require './proxyvisor'
|
||||
hostConfig = require './host-config'
|
||||
|
||||
module.exports = (application) ->
|
||||
authenticate = (req, res, next) ->
|
||||
@ -208,7 +209,6 @@ module.exports = (application) ->
|
||||
.catch (err) ->
|
||||
res.status(503).send(err?.message or err or 'Unknown error')
|
||||
|
||||
|
||||
# Expires the supervisor's API key and generates a new one.
|
||||
# It also communicates the new key to the Resin API.
|
||||
unparsedRouter.post '/v1/regenerate-api-key', (req, res) ->
|
||||
@ -219,6 +219,20 @@ module.exports = (application) ->
|
||||
.catch (err) ->
|
||||
res.status(503).send(err?.message or err or 'Unknown error')
|
||||
|
||||
parsedRouter.get '/v1/device/host-config', (req, res) ->
|
||||
hostConfig.get()
|
||||
.then (conf) ->
|
||||
res.json(conf)
|
||||
.catch (err) ->
|
||||
res.status(503).send(err?.message or err or 'Unknown error')
|
||||
|
||||
parsedRouter.patch '/v1/device/host-config', (req, res) ->
|
||||
hostConfig.patch(req.body)
|
||||
.then ->
|
||||
res.status(200).send('OK')
|
||||
.catch (err) ->
|
||||
res.status(503).send(err?.message or err or 'Unknown error')
|
||||
|
||||
unparsedRouter.get '/v1/device', (req, res) ->
|
||||
res.json(device.getState())
|
||||
|
||||
|
@ -5,7 +5,6 @@ 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'
|
||||
@ -13,23 +12,13 @@ TypedError = require 'typed-error'
|
||||
osRelease = require './lib/os-release'
|
||||
semver = require 'semver'
|
||||
semverRegex = require('semver-regex')
|
||||
|
||||
userConfig = {}
|
||||
configJson = require './config-json'
|
||||
|
||||
DuplicateUuidError = (err) -> _.startsWith(err.message, '"uuid" must be unique')
|
||||
exports.ExchangeKeyError = class ExchangeKeyError extends TypedError
|
||||
|
||||
bootstrapper = {}
|
||||
|
||||
writeAndSyncFile = (path, data) ->
|
||||
fs.openAsync(path, 'w')
|
||||
.then (fd) ->
|
||||
fs.writeAsync(fd, data, 0, 'utf8')
|
||||
.then ->
|
||||
fs.fsyncAsync(fd)
|
||||
.then ->
|
||||
fs.closeAsync(fd)
|
||||
|
||||
loadPreloadedApps = ->
|
||||
devConfig = {}
|
||||
knex('app').select()
|
||||
@ -37,27 +26,29 @@ loadPreloadedApps = ->
|
||||
if apps.length > 0
|
||||
console.log('Preloaded apps already loaded, skipping')
|
||||
return
|
||||
fs.readFileAsync(appsPath, 'utf8')
|
||||
.then(JSON.parse)
|
||||
.map (app) ->
|
||||
utils.extendEnvVars(app.env, userConfig.uuid, userConfig.deviceApiKey, app.appId, app.name, app.commit)
|
||||
.then (extendedEnv) ->
|
||||
app.env = JSON.stringify(extendedEnv)
|
||||
app.markedForDeletion = false
|
||||
_.merge(devConfig, app.config)
|
||||
app.config = JSON.stringify(app.config)
|
||||
knex('app').insert(app)
|
||||
.then ->
|
||||
deviceConfig.set({ targetValues: devConfig })
|
||||
configJson.getAll()
|
||||
.then (userConfig) ->
|
||||
fs.readFileAsync(appsPath, 'utf8')
|
||||
.then(JSON.parse)
|
||||
.map (app) ->
|
||||
utils.extendEnvVars(app.env, userConfig.uuid, userConfig.deviceApiKey, app.appId, app.name, app.commit)
|
||||
.then (extendedEnv) ->
|
||||
app.env = JSON.stringify(extendedEnv)
|
||||
app.markedForDeletion = false
|
||||
_.merge(devConfig, app.config)
|
||||
app.config = JSON.stringify(app.config)
|
||||
knex('app').insert(app)
|
||||
.then ->
|
||||
deviceConfig.set({ targetValues: devConfig })
|
||||
.catch (err) ->
|
||||
utils.mixpanelTrack('Loading preloaded apps failed', { error: err })
|
||||
|
||||
fetchDevice = (apiKey) ->
|
||||
fetchDevice = (uuid, apiKey) ->
|
||||
resinApi.get
|
||||
resource: 'device'
|
||||
options:
|
||||
filter:
|
||||
uuid: userConfig.uuid
|
||||
uuid: uuid
|
||||
customOptions:
|
||||
apikey: apiKey
|
||||
.get(0)
|
||||
@ -65,33 +56,47 @@ fetchDevice = (apiKey) ->
|
||||
.timeout(config.apiTimeout)
|
||||
|
||||
exchangeKey = ->
|
||||
Promise.try ->
|
||||
# If we have an existing device key we first check if it's valid, because if it is we can just use that
|
||||
if userConfig.deviceApiKey?
|
||||
fetchDevice(userConfig.deviceApiKey)
|
||||
.then (device) ->
|
||||
if device?
|
||||
return device
|
||||
# If it's not valid/doesn't exist then we try to use the user/provisioning api key for the exchange
|
||||
fetchDevice(userConfig.apiKey)
|
||||
configJson.getAll()
|
||||
.then (userConfig) ->
|
||||
Promise.try ->
|
||||
# If we have an existing device key we first check if it's valid, because if it is we can just use that
|
||||
if userConfig.deviceApiKey?
|
||||
fetchDevice(userConfig.uuid, userConfig.deviceApiKey)
|
||||
.then (device) ->
|
||||
if not device?
|
||||
throw new ExchangeKeyError("Couldn't fetch device with provisioning key")
|
||||
# We found the device, we can try to register a working device key for it
|
||||
userConfig.deviceApiKey ?= deviceRegister.generateUniqueKey()
|
||||
request.postAsync("#{config.apiEndpoint}/api-key/device/#{device.id}/device-key?apikey=#{userConfig.apiKey}", {
|
||||
json: true
|
||||
body:
|
||||
apiKey: userConfig.deviceApiKey
|
||||
})
|
||||
.spread (res, body) ->
|
||||
if res.statusCode != 200
|
||||
throw new ExchangeKeyError("Couldn't register device key with provisioning key")
|
||||
.return(device)
|
||||
if device?
|
||||
return device
|
||||
# If it's not valid/doesn't exist then we try to use the user/provisioning api key for the exchange
|
||||
fetchDevice(userConfig.uuid, userConfig.apiKey)
|
||||
.then (device) ->
|
||||
if not device?
|
||||
throw new ExchangeKeyError("Couldn't fetch device with provisioning key")
|
||||
# We found the device, we can try to register a working device key for it
|
||||
Promise.try ->
|
||||
if !userConfig.deviceApiKey?
|
||||
deviceApiKey = deviceRegister.generateUniqueKey()
|
||||
configJson.set({ deviceApiKey })
|
||||
.return(deviceApiKey)
|
||||
else
|
||||
return userConfig.deviceApiKey
|
||||
.then (deviceApiKey) ->
|
||||
request.postAsync("#{config.apiEndpoint}/api-key/device/#{device.id}/device-key?apikey=#{userConfig.apiKey}", {
|
||||
json: true
|
||||
body:
|
||||
apiKey: deviceApiKey
|
||||
})
|
||||
.spread (res, body) ->
|
||||
if res.statusCode != 200
|
||||
throw new ExchangeKeyError("Couldn't register device key with provisioning key")
|
||||
.return(device)
|
||||
|
||||
bootstrap = ->
|
||||
Promise.try ->
|
||||
userConfig.deviceType ?= 'raspberry-pi'
|
||||
configJson.get('deviceType')
|
||||
.then (deviceType) ->
|
||||
if !deviceType?
|
||||
configJson.set(deviceType: 'raspberry-pi')
|
||||
.then ->
|
||||
configJson.getAll()
|
||||
.then (userConfig) ->
|
||||
if userConfig.registered_at?
|
||||
return userConfig
|
||||
|
||||
@ -115,18 +120,22 @@ bootstrap = ->
|
||||
console.log('Exchanging key failed, having to reregister')
|
||||
generateRegistration(true)
|
||||
.then ({ id }) ->
|
||||
userConfig.registered_at = Date.now()
|
||||
userConfig.deviceId = id
|
||||
toUpdate = {}
|
||||
toDelete = []
|
||||
if !userConfig.registered_at?
|
||||
toUpdate.registered_at = Date.now()
|
||||
toUpdate.deviceId = id
|
||||
osRelease.getOSVersion(config.hostOSVersionPath)
|
||||
.then (osVersion) ->
|
||||
# Delete the provisioning key now, only if the OS supports it
|
||||
hasSupport = hasDeviceApiKeySupport(osVersion)
|
||||
if hasSupport
|
||||
delete userConfig.apiKey
|
||||
else
|
||||
userConfig.apiKey = userConfig.deviceApiKey
|
||||
writeAndSyncFile(configPath, JSON.stringify(userConfig))
|
||||
.return(userConfig)
|
||||
.then (osVersion) ->
|
||||
# Delete the provisioning key now, only if the OS supports it
|
||||
hasSupport = hasDeviceApiKeySupport(osVersion)
|
||||
if hasSupport
|
||||
toDelete.push('apiKey')
|
||||
else
|
||||
toUpdate.apiKey = userConfig.deviceApiKey
|
||||
configJson.set(toUpdate, toDelete)
|
||||
.then ->
|
||||
configJson.getAll()
|
||||
.then (userConfig) ->
|
||||
console.log('Finishing bootstrapping')
|
||||
knex('config').whereIn('key', ['uuid', 'apiKey', 'username', 'userId', 'version']).delete()
|
||||
@ -144,20 +153,18 @@ bootstrap = ->
|
||||
.tap ->
|
||||
bootstrapper.doneBootstrapping()
|
||||
|
||||
readConfig = ->
|
||||
fs.readFileAsync(configPath, 'utf8')
|
||||
.then(JSON.parse)
|
||||
|
||||
generateRegistration = (forceReregister = false) ->
|
||||
Promise.try ->
|
||||
if forceReregister
|
||||
userConfig.uuid = deviceRegister.generateUniqueKey()
|
||||
userConfig.deviceApiKey = deviceRegister.generateUniqueKey()
|
||||
configJson.set({ uuid: deviceRegister.generateUniqueKey(), deviceApiKey: deviceRegister.generateUniqueKey() })
|
||||
else
|
||||
userConfig.uuid ?= deviceRegister.generateUniqueKey()
|
||||
userConfig.deviceApiKey ?= deviceRegister.generateUniqueKey()
|
||||
writeAndSyncFile(configPath, JSON.stringify(userConfig))
|
||||
.return(userConfig.uuid)
|
||||
configJson.getAll()
|
||||
.then ({ uuid, deviceApiKey }) ->
|
||||
uuid ?= deviceRegister.generateUniqueKey()
|
||||
deviceApiKey ?= deviceRegister.generateUniqueKey()
|
||||
configJson.set({ uuid, deviceApiKey })
|
||||
.then ->
|
||||
configJson.get('uuid')
|
||||
.catch (err) ->
|
||||
console.log('Error generating and saving UUID: ', err)
|
||||
Promise.delay(config.bootstrapRetryDelay)
|
||||
@ -186,23 +193,27 @@ exchangeKeyAndUpdateConfig = ->
|
||||
# Otherwise VPN and other host services that use an API key will break.
|
||||
#
|
||||
# In other cases, we make the apiKey equal the deviceApiKey instead.
|
||||
osRelease.getOSVersion(config.hostOSVersionPath)
|
||||
.then (osVersion) ->
|
||||
hasSupport = hasDeviceApiKeySupport(osVersion)
|
||||
if hasSupport or userConfig.apiKey != userConfig.deviceApiKey
|
||||
console.log('Attempting key exchange')
|
||||
exchangeKey()
|
||||
.then ->
|
||||
console.log('Key exchange succeeded, starting to use deviceApiKey')
|
||||
if hasSupport
|
||||
delete userConfig.apiKey
|
||||
else
|
||||
userConfig.apiKey = userConfig.deviceApiKey
|
||||
utils.setConfig('deviceApiKey', userConfig.deviceApiKey)
|
||||
.then ->
|
||||
utils.setConfig('apiKey', userConfig.deviceApiKey)
|
||||
.then ->
|
||||
writeAndSyncFile(configPath, JSON.stringify(userConfig))
|
||||
Promise.join(
|
||||
configJson.getAll()
|
||||
osRelease.getOSVersion(config.hostOSVersionPath)
|
||||
(userConfig, osVersion) ->
|
||||
hasSupport = hasDeviceApiKeySupport(osVersion)
|
||||
if hasSupport or userConfig.apiKey != userConfig.deviceApiKey
|
||||
console.log('Attempting key exchange')
|
||||
exchangeKey()
|
||||
.then ->
|
||||
configJson.get('deviceApiKey')
|
||||
.then (deviceApiKey) ->
|
||||
console.log('Key exchange succeeded, starting to use deviceApiKey')
|
||||
utils.setConfig('deviceApiKey', deviceApiKey)
|
||||
.then ->
|
||||
utils.setConfig('apiKey', deviceApiKey)
|
||||
.then ->
|
||||
if hasSupport
|
||||
configJson.set({}, [ 'apiKey' ])
|
||||
else
|
||||
configJson.set(apiKey: deviceApiKey)
|
||||
)
|
||||
|
||||
exchangeKeyOrRetry = do ->
|
||||
_failedExchanges = 0
|
||||
@ -217,31 +228,34 @@ exchangeKeyOrRetry = do ->
|
||||
|
||||
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
|
||||
# This will only be the case when the supervisor/OS has been updated.
|
||||
if userConfig.apiKey?
|
||||
exchangeKeyOrRetry()
|
||||
else
|
||||
Promise.join(
|
||||
knex('config').select('value').where(key: 'apiKey')
|
||||
knex('config').select('value').where(key: 'deviceApiKey')
|
||||
([ apiKey ], [ deviceApiKey ]) ->
|
||||
if !deviceApiKey?.value
|
||||
# apiKey in the DB is actually the deviceApiKey, but it was
|
||||
# exchanged in a supervisor version that didn't save it to the DB
|
||||
# (which mainly affects the RESIN_API_KEY env var)
|
||||
knex('config').insert({ key: 'deviceApiKey', value: apiKey.value })
|
||||
)
|
||||
configJson.getAll()
|
||||
.then (userConfig) ->
|
||||
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
|
||||
# This will only be the case when the supervisor/OS has been updated.
|
||||
if userConfig.apiKey?
|
||||
exchangeKeyOrRetry()
|
||||
else
|
||||
Promise.join(
|
||||
knex('config').select('value').where(key: 'apiKey')
|
||||
knex('config').select('value').where(key: 'deviceApiKey')
|
||||
([ apiKey ], [ deviceApiKey ]) ->
|
||||
if !deviceApiKey?.value
|
||||
# apiKey in the DB is actually the deviceApiKey, but it was
|
||||
# exchanged in a supervisor version that didn't save it to the DB
|
||||
# (which mainly affects the RESIN_API_KEY env var)
|
||||
knex('config').insert({ key: 'deviceApiKey', value: apiKey.value })
|
||||
)
|
||||
return
|
||||
|
||||
bootstrapper.bootstrapped = false
|
||||
bootstrapper.startBootstrapping = ->
|
||||
# Load config file
|
||||
readConfig()
|
||||
.then (configFromFile) ->
|
||||
userConfig = configFromFile
|
||||
configJson.init()
|
||||
.then ->
|
||||
configJson.getAll()
|
||||
.then (userConfig) ->
|
||||
bootstrapper.offlineMode = !Boolean(config.apiEndpoint) or Boolean(userConfig.supervisorOfflineMode)
|
||||
knex('config').select('value').where(key: 'uuid')
|
||||
.then ([ uuid ]) ->
|
||||
|
46
src/config-json.coffee
Normal file
46
src/config-json.coffee
Normal file
@ -0,0 +1,46 @@
|
||||
Promise = require 'bluebird'
|
||||
_ = require 'lodash'
|
||||
configPath = '/boot/config.json'
|
||||
Lock = require 'rwlock'
|
||||
fs = Promise.promisifyAll(require('fs'))
|
||||
{ writeAndSyncFile } = require './lib/fs-utils'
|
||||
|
||||
lock = new Lock()
|
||||
writeLock = Promise.promisify(lock.async.writeLock)
|
||||
readLock = Promise.promisify(lock.async.readLock)
|
||||
withWriteLock = do ->
|
||||
_takeLock = ->
|
||||
writeLock('config')
|
||||
.disposer (release) ->
|
||||
release()
|
||||
return (func) ->
|
||||
Promise.using(_takeLock(), func)
|
||||
withReadLock = do ->
|
||||
_takeLock = ->
|
||||
readLock('config')
|
||||
.disposer (release) ->
|
||||
release()
|
||||
return (func) ->
|
||||
Promise.using(_takeLock(), func)
|
||||
|
||||
# write-through cache of the config.json file
|
||||
userConfig = null
|
||||
exports.init = ->
|
||||
fs.readFileAsync(configPath)
|
||||
.then (conf) ->
|
||||
userConfig = JSON.parse(conf)
|
||||
|
||||
exports.get = (key) ->
|
||||
withReadLock ->
|
||||
return userConfig[key]
|
||||
|
||||
exports.getAll = ->
|
||||
withReadLock ->
|
||||
return _.clone(userConfig)
|
||||
|
||||
exports.set = (vals = {}, keysToDelete = []) ->
|
||||
withWriteLock ->
|
||||
_.merge(userConfig, vals)
|
||||
for key in keysToDelete
|
||||
delete userConfig[key]
|
||||
writeAndSyncFile(configPath, JSON.stringify(userConfig))
|
128
src/host-config.coffee
Normal file
128
src/host-config.coffee
Normal file
@ -0,0 +1,128 @@
|
||||
Promise = require 'bluebird'
|
||||
_ = require 'lodash'
|
||||
utils = require './utils'
|
||||
path = require 'path'
|
||||
config = require './config'
|
||||
fs = Promise.promisifyAll(require('fs'))
|
||||
configJson = require './config-json'
|
||||
{ writeFileAtomic } = require './lib/fs-utils'
|
||||
mkdirp = Promise.promisify(require('mkdirp'))
|
||||
|
||||
ENOENT = (err) -> err.code is 'ENOENT'
|
||||
|
||||
redsocksHeader = '''
|
||||
base {
|
||||
log_debug = off;
|
||||
log_info = on;
|
||||
log = stderr;
|
||||
daemon = off;
|
||||
redirector = iptables;
|
||||
}
|
||||
|
||||
redsocks {
|
||||
local_ip = 127.0.0.1;
|
||||
local_port = 12345;
|
||||
|
||||
'''
|
||||
|
||||
redsocksFooter = '}\n'
|
||||
|
||||
proxyFields = [ 'type', 'ip', 'port', 'login', 'password' ]
|
||||
|
||||
proxyBasePath = path.join('/mnt/root', config.bootMountPoint, 'system-proxy')
|
||||
redsocksConfPath = path.join(proxyBasePath, 'redsocks.conf')
|
||||
noProxyPath = path.join(proxyBasePath, 'no_proxy')
|
||||
|
||||
readProxy = ->
|
||||
fs.readFileAsync(redsocksConfPath)
|
||||
.then (redsocksConf) ->
|
||||
lines = new String(redsocksConf).split('\n')
|
||||
conf = {}
|
||||
for line in lines
|
||||
for proxyField in proxyFields
|
||||
if proxyField in [ 'login', 'password' ]
|
||||
m = line.match(new RegExp(proxyField + '\\s*=\\s*\"(.*)\"\\s*;'))
|
||||
else
|
||||
m = line.match(new RegExp(proxyField + '\\s*=\\s*([^;\\s]*)\\s*;'))
|
||||
if m?
|
||||
conf[proxyField] = m[1]
|
||||
return conf
|
||||
.catch ENOENT, ->
|
||||
return null
|
||||
.then (conf) ->
|
||||
if !conf?
|
||||
return null
|
||||
else
|
||||
fs.readFileAsync(noProxyPath)
|
||||
.then (noProxy) ->
|
||||
conf.noProxy = new String(noProxy).split('\n')
|
||||
return conf
|
||||
.catch ENOENT, ->
|
||||
return conf
|
||||
|
||||
generateRedsocksConfEntries = (conf) ->
|
||||
val = ''
|
||||
for field in proxyFields
|
||||
if conf[field]?
|
||||
v = conf[field]
|
||||
if field in [ 'login', 'password' ]
|
||||
v = "\"#{v}\""
|
||||
val += "\t#{field} = #{v};\n"
|
||||
return val
|
||||
|
||||
setProxy = (conf) ->
|
||||
Promise.try ->
|
||||
if _.isEmpty(conf)
|
||||
fs.unlinkAsync(redsocksConfPath)
|
||||
.catch(ENOENT, _.noop)
|
||||
.then ->
|
||||
fs.unlinkAsync(noProxyPath)
|
||||
.catch(ENOENT, _.noop)
|
||||
else
|
||||
mkdirp(proxyBasePath)
|
||||
.then ->
|
||||
if _.isArray(conf.noProxy)
|
||||
writeFileAtomic(noProxyPath, conf.noProxy.join('\n'))
|
||||
.then ->
|
||||
redsocksConf = ''
|
||||
redsocksConf += redsocksHeader
|
||||
redsocksConf += generateRedsocksConfEntries(conf)
|
||||
redsocksConf += redsocksFooter
|
||||
writeFileAtomic(redsocksConfPath, redsocksConf)
|
||||
.then ->
|
||||
utils.restartSystemdService('resin-proxy-config')
|
||||
.then ->
|
||||
utils.restartSystemdService('redsocks')
|
||||
|
||||
hostnamePath = '/mnt/root/etc/hostname'
|
||||
readHostname = ->
|
||||
fs.readFileAsync(hostnamePath)
|
||||
.then (hostnameData) ->
|
||||
return _.trim(new String(hostnameData))
|
||||
|
||||
setHostname = (val) ->
|
||||
configJson.set(hostname: val)
|
||||
.then ->
|
||||
utils.restartSystemdService('resin-hostname')
|
||||
|
||||
|
||||
exports.get = ->
|
||||
Promise.join(
|
||||
readProxy()
|
||||
readHostname()
|
||||
(proxy, hostname) ->
|
||||
return {
|
||||
network: {
|
||||
proxy
|
||||
hostname
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
exports.patch = (conf) ->
|
||||
Promise.try ->
|
||||
if !_.isUndefined(conf?.network?.proxy)
|
||||
setProxy(conf.network.proxy)
|
||||
.then ->
|
||||
if !_.isUndefined(conf?.network?.hostname)
|
||||
setHostname(conf.network.hostname)
|
16
src/lib/fs-utils.coffee
Normal file
16
src/lib/fs-utils.coffee
Normal file
@ -0,0 +1,16 @@
|
||||
Promise = require 'bluebird'
|
||||
fs = Promise.promisifyAll(require('fs'))
|
||||
|
||||
exports.writeAndSyncFile = (path, data) ->
|
||||
fs.openAsync(path, 'w')
|
||||
.then (fd) ->
|
||||
fs.writeAsync(fd, data, 0, 'utf8')
|
||||
.then ->
|
||||
fs.fsyncAsync(fd)
|
||||
.then ->
|
||||
fs.closeAsync(fd)
|
||||
|
||||
exports.writeFileAtomic = (path, data) ->
|
||||
exports.writeAndSyncFile("#{path}.new", data)
|
||||
.then ->
|
||||
fs.renameAsync("#{path}.new", path)
|
@ -236,6 +236,14 @@ exports.vpnControl = (val, logMessage, { initial = false } = {}) ->
|
||||
return false
|
||||
.catchReturn(false)
|
||||
|
||||
exports.restartSystemdService = (serviceName) ->
|
||||
gosuper.postAsync('/v1/restart-service', { json: true, body: Name: serviceName })
|
||||
.spread (response, body) ->
|
||||
if response.statusCode != 200
|
||||
err = new Error("Error restarting service #{serviceName}: #{response.statusCode} #{body}")
|
||||
err.statusCode = response.statusCode
|
||||
throw err
|
||||
|
||||
exports.AppNotFoundError = class AppNotFoundError extends TypedError
|
||||
|
||||
exports.getKnexApp = (appId, columns) ->
|
||||
|
Loading…
x
Reference in New Issue
Block a user